module SlackBuilder.Info ( PackageInfo(..) , generate , parseInfoFile ) where import Control.Applicative (Alternative(..)) import Control.Monad.Combinators (sepBy) import qualified Data.ByteArray as ByteArray import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as Char8 import Data.Maybe (mapMaybe) import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as Lazy.Text import qualified Data.Text.Lazy.Builder as Text.Builder import qualified Data.Text.Lazy.Builder as Text (Builder) import Crypto.Hash (Digest, MD5, digestFromByteString) import Data.Void (Void) import Data.Word (Word8) import Numeric (readHex, showHex) import Text.Megaparsec (Parsec, count, eof, takeWhile1P) import Text.Megaparsec.Byte (space, string, hexDigitChar) import Text.URI ( URI(..) , parserBs , render ) import qualified Data.Word8 as Word8 type GenParser = Parsec Void ByteString -- | Data used to generate an .info file. data PackageInfo = PackageInfo { pkgname :: String , version :: Text , homepage :: Text , downloads :: [URI] , checksums :: [Digest MD5] , downloadX64 :: [URI] , checksumX64 :: [Digest MD5] , requires :: [ByteString] , maintainer :: Text , email :: Text } deriving (Eq, Show) variableEntry :: ByteString -> GenParser ByteString variableEntry variable = string (Char8.append variable "=\"") *> takeWhile1P Nothing (0x22 /=) <* string "\"\n" variableSeparator :: GenParser () variableSeparator = string " \\" *> space packageDownloads :: ByteString -> GenParser [URI] packageDownloads variableName = string (variableName <> "=\"") *> sepBy parserBs variableSeparator <* string "\"\n" hexDigit :: GenParser Word8 hexDigit = let digitPair = count 2 hexDigitChar in fst . head . readHex . fmap (toEnum . fromIntegral) <$> digitPair packageChecksum :: GenParser ByteString packageChecksum = ByteString.pack <$> count 16 hexDigit packageChecksums :: ByteString -> GenParser [ByteString] packageChecksums variableName = string (variableName <> "=\"") *> sepBy packageChecksum variableSeparator <* string "\"\n" packageRequires :: GenParser [ByteString] packageRequires = string "REQUIRES=\"" *> sepBy (packageName <|> string "%README%") space <* string "\"\n" packageName :: GenParser ByteString packageName = takeWhile1P Nothing isNameToken where isNameToken x = Word8.isAlphaNum x || x == Word8._hyphen || x == Word8._underscore parseInfoFile :: GenParser PackageInfo parseInfoFile = PackageInfo <$> (Char8.unpack <$> packagePrgnam) <*> (Text.decodeUtf8 <$> variableEntry "VERSION") <*> (Text.decodeUtf8 <$> variableEntry "HOMEPAGE") <*> packageDownloads "DOWNLOAD" <*> (mapMaybe digestFromByteString <$> packageChecksums "MD5SUM") <*> packageDownloads "DOWNLOAD_x86_64" <*> (mapMaybe digestFromByteString <$> packageChecksums "MD5SUM_x86_64") <*> packageRequires <*> (Text.decodeUtf8 <$> variableEntry "MAINTAINER") <*> (Text.decodeUtf8 <$> variableEntry "EMAIL") <* eof where packagePrgnam = (string "PKGNAM" <|> string "PRGNAM") >> string "=\"" *> packageName <* "\"\n" generate :: PackageInfo -> Text generate pkg = Lazy.Text.toStrict $ Text.Builder.toLazyText builder where digestToText = Text.pack . foldr hexAppender "" . ByteArray.unpack hexAppender x acc | x > 15 = showHex x acc | otherwise = '0' : showHex x acc builder = "PRGNAM=\"" <> Text.Builder.fromString (pkgname pkg) <> "\"\n" <> "VERSION=\"" <> Text.Builder.fromText (version pkg) <> "\"\n" <> "HOMEPAGE=\"" <> Text.Builder.fromText (homepage pkg) <> "\"\n" <> generateMultiEntry "DOWNLOAD" (render <$> downloads pkg) <> generateMultiEntry "MD5SUM" (digestToText <$> checksums pkg) <> generateMultiEntry "DOWNLOAD_x86_64" (render <$> downloadX64 pkg) <> generateMultiEntry "MD5SUM_x86_64" (digestToText <$> checksumX64 pkg) <> "REQUIRES=\"" <> fromByteStringWords (requires pkg) <> "\"\n" <> "MAINTAINER=\"" <> Text.Builder.fromText (maintainer pkg) <> "\"\n" <> "EMAIL=\"" <> Text.Builder.fromText (email pkg) <> "\"\n" fromByteStringWords = Text.Builder.fromText . Text.unwords . fmap Text.decodeUtf8 generateMultiEntry :: Text -> [Text] -> Text.Builder generateMultiEntry name entries = Text.Builder.fromText name <> "=\"" <> Text.Builder.fromText (Text.intercalate separator entries) <> "\"\n" where padLength = Text.length name + 2 separator = " \\\n" <> Text.replicate padLength " "