packages feed

module-management-0.20.2: testdata/debian/Debian/Repo/Changes.hs

-- | A .changes file is a file generated by dpkg-buildpackage
-- describing the result of a package build.  Not to be confused
-- with a debian/changelog file.  After it is uploaded it goes into
-- the @installed/@ subdirectory of the repository.
{-# LANGUAGE PackageImports, OverloadedStrings #-}
{-# OPTIONS -fno-warn-name-shadowing -fno-warn-missing-signatures #-}
module Debian.Repo.Changes
    ( findChangesFiles
    , saveChangesFile
{-
    , parseChangesFilename
    , parseChangesFile
-}
    , changeKey
    , changePath
    , changeName
    ) where

import Data.List (isSuffixOf)
import Data.Maybe (catMaybes)
import Data.Monoid ((<>), mconcat)
import Data.Text (pack, Text, unpack)
import Debian.Arch (Arch, parseArch, prettyArch)
import Debian.Changes (ChangedFileSpec(..), ChangesFile(..), changesFileName, parseChanges)
import qualified Debian.Control.Text as S (Control'(Control), ControlFunctions(parseControlFromFile), fieldValue, modifyField, Paragraph'(..))
import Debian.Pretty (ppDisplay)
import Debian.Release (parseReleaseName, parseSection)
import Debian.Repo.Prelude (replaceFile)
import Debian.URI ()
import Debian.Version (DebianVersion, parseDebianVersion, prettyDebianVersion)
import System.Directory (getDirectoryContents)
import Text.Regex (matchRegex, matchRegexAll, mkRegex)

findChangesFiles :: FilePath -> IO [ChangesFile]
findChangesFiles dir =
    getDirectoryContents dir >>=
    return . filter (isSuffixOf ".changes") >>=
    mapM (loadChangesFile dir) >>= return . catMaybes

loadChangesFile :: FilePath -> String -> IO (Maybe ChangesFile)
loadChangesFile dir file =
    do
      case parseChangesFilename file of
        Just (name, ver, arch) ->
            do
              result <- parseChangesFile dir file
              case result of
                Right (S.Control changes) ->
                    -- The .changes file should be a single paragraph,
                    -- but there have been instances where extra newlines
                    -- are inserted.  To be forgiving we will concat all
                    -- the paragraphs into one (rather than erroring out
                    -- or discarding all but the first paragraph.)
                    let changes' = mergeParagraphs changes in
                    case (S.fieldValue "Files" changes' :: Maybe Text,
                          S.fieldValue "Checksums-Sha1" changes' :: Maybe Text,
                          S.fieldValue "Checksums-Sha256" changes' :: Maybe Text,
                          maybe Nothing parseChanges (S.fieldValue "Changes" changes'),
                          S.fieldValue "Distribution" changes') of
                      (Just text, sha1text, sha256text, Just entry, Just release) ->
                          do return . Just $ Changes { changeDir = dir
                                                     , changePackage = name
                                                     , changeVersion = ver
                                                     , changeRelease = parseReleaseName (unpack release)
                                                     , changeArch = arch
                                                     , changeInfo = changes'
                                                     , changeEntry = entry
                                                     , changeFiles = changedFileSpecs text sha1text sha256text }
                      _ -> return Nothing	-- Missing 'Files', 'Checksums-Sha1', 'Checksums-Sha256', 'Changes', or 'Distribution' field in .changes
                Left _error -> return Nothing
        Nothing -> return Nothing		-- Couldn't parse changes filename

mergeParagraphs :: [S.Paragraph' Text] -> S.Paragraph' Text
mergeParagraphs paragraphs =
    S.Paragraph . concat . map fieldsOf $ paragraphs
    where fieldsOf (S.Paragraph fields) = fields

saveChangesFile :: ChangesFile -> IO ()
saveChangesFile changes =
    replaceFile path (ppDisplay (updateFiles (changeFiles changes) (changeInfo changes)))
    where
      path = changeDir changes ++ "/" ++ changesFileName changes
      updateFiles files info =
          S.modifyField "Checksums-Sha1" (const (showSHA1List files)).
          S.modifyField "Checksums-Sha256" (const (showSHA256List files)) .
          S.modifyField "Files" (const (showFileList files)) $ info

-- | They primary key for .changes files is the triple containing the
-- package name, version, and architecture.  There is a 1-1 correspondence
-- between a key value and the name of the .changes file.
changeKey :: ChangesFile -> (String, DebianVersion, Arch)
changeKey changes = (changePackage changes, changeVersion changes, changeArch changes)

changeName :: ChangesFile -> FilePath
changeName changes =
    changePackage changes <> "_" <>
    show (prettyDebianVersion (changeVersion changes)) <> "_" <>
    show (prettyArch (changeArch changes)) <> ".changes"

changePath :: ChangesFile -> FilePath
changePath changes = changeDir changes ++ "/" ++ changeName changes

-- | I suspect it would be more correct to read the contents of the file and
-- use that to construct the info we are inferring here from the filename.
parseChangesFilename :: String -> Maybe (String, DebianVersion, Arch)
parseChangesFilename name =
    case matchRegex (mkRegex "^(.*/)?([^_]*)_(.*)_([^.]*)\\.changes$") name of
      Just [_, name, version, arch] -> Just (name, parseDebianVersion version, parseArch arch)
      _ -> error ("Invalid .changes file name: " ++ name)

-- parseChangesFile :: FilePath -> String -> IO (Either ParseError S.Control)
parseChangesFile dir file = S.parseControlFromFile (dir ++ "/" ++ file)

changedFileSpecs :: Text -> Maybe Text -> Maybe Text -> [ChangedFileSpec]
changedFileSpecs text sha1text sha256text =
    map changedFileSpec fileInfo
    where
      changedFileSpec (md5sum, size, section, priority, filename) =
                ChangedFileSpec { changedFileMD5sum = md5sum
                                , changedFileSHA1sum = maybe "" fst (lookup filename sha1sums)
                                , changedFileSHA256sum = maybe "" fst (lookup filename sha256sums)
                                , changedFileSize = read size
                                , changedFileSection = parseSection section
                                , changedFilePriority = priority
                                , changedFileName = filename }
      fileInfo = parseFileList (unpack text)
      sha1sums = maybe [] (parseChecksums . unpack) sha1text
      sha256sums = maybe [] (parseChecksums . unpack) sha256text

parseFileList :: String -> [(String, String, String, String, String)]
parseFileList text =
    case matchRegexAll re text of
      Just (_, _, remaining, [md5sum, size, section, priority, filename]) ->
          (md5sum, size, section, priority, filename) : parseFileList remaining
      _ -> []
    where
      re = mkRegex ("^[ \t\n]*" ++ g ++w++ g ++w++ g ++w++ g ++w++ g ++ "[ \t\n]*")
      g = "(" ++ t ++ ")"
      t = "[^ \t\n]+"
      w = "[ \t]+"

parseChecksums :: String -> [(String, (String, String))]
parseChecksums text =
    case matchRegexAll re text of
      Just (_, _, remaining, [checksum, size, name]) -> (name, (checksum, size)) : parseChecksums remaining
      _ -> []
    where
      re = mkRegex ("^[ \t\n]*" ++ g ++w++ g ++w++ g ++ "[ \t\n]*")
      g = "(" ++ t ++ ")"
      t = "[^ \t\n]+"
      w = "[ \t]+"

showFileList :: [ChangedFileSpec] -> Text
showFileList files = pack $ concat (map (("\n " <>) . ppDisplay) files)

showSHA1List :: [ChangedFileSpec] -> Text
showSHA1List files = mconcat (map (("\n " <>) . showSHA1) files)
    where showSHA1 x = pack $ changedFileSHA1sum x ++ " " ++ show (changedFileSize x) ++ " " ++ changedFileName x

showSHA256List :: [ChangedFileSpec] -> Text
showSHA256List files = mconcat (map (("\n " <>) . showSHA256) files)
    where showSHA256 x = pack $ changedFileSHA256sum x ++ " " ++ show (changedFileSize x) ++ " " ++ changedFileName x