{-# LANGUAGE FlexibleInstances, OverloadedStrings, StandaloneDeriving, TemplateHaskell #-} module TAR where import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as ASCII import Data.Char (isOctDigit) import Data.Foldable (fold) import Data.Functor.Identity (Identity(Identity, runIdentity)) import Data.Monoid.Textual (TextualMonoid) import qualified Data.Monoid.Textual import Data.Text (Text) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Word import Numeric (readOct, showOct) import qualified Rank2.TH import Data.Attoparsec.ByteString (Parser) import Construct import Prelude hiding ((<$), (<*), (*>), take, takeWhile) data Archive f = Archive{files :: f [File Identity]} data File f = File{ header :: f (FileHeader () Identity), content :: f ByteString} data FileType = NormalFile | HardLink | SymLink | CharSpecial | BlockSpecial | Directory | FIFO | ContiguousFile | ExtendedGlobalHeader | ExtendedFileHeader | VendorExtension{vendorCode :: Word8} | Reserved{reservedCode :: Word8} deriving (Eq, Read, Show) data FileHeader cksum f = FileHeader{ fileName :: f Text, fileMode :: f Word32, ownerID :: f Word32, groupID :: f Word32, fileSize :: f Word64, modified :: f Word64, checksum :: f cksum, fileType :: f FileType, linked :: f Text, ustar :: f (Maybe (UStarHeader Identity))} data UStarHeader f = UStarHeader{ ownerName :: f Text, groupName :: f Text, deviceMajor :: f (Maybe Word32), deviceMinor :: f (Maybe Word32), namePrefix :: f Text} deriving instance Show (Archive Identity) deriving instance Show (File Identity) deriving instance Show (FileHeader () Identity) deriving instance Show (FileHeader Word32 Identity) deriving instance Show (UStarHeader Identity) archive :: Format Parser Maybe ByteString (Archive Identity) archive = record Archive{files= many file} file :: Format Parser Maybe ByteString (File Identity) file = mapValue (uncurry file) (\f-> (runIdentity $ header f, runIdentity $ content f)) $ deppair fileHeader (\FileHeader{fileSize= size}-> ByteString.replicate (((fromIntegral size + 511) `div` 512) * 512) 0 `padded` take (fromIntegral size)) where file h c = File (Identity h) (Identity c) fileHeader :: Format Parser Maybe ByteString (FileHeader () Identity) fileHeader = mapMaybeValue validate calculate (record $ fileHeaderRecord (zeroDelimitedOctal 7 <* value char ' ')) where validate header | header' <- header{checksum= pure ()}, fromIntegral (checksum header) == sumOf header' = Just header' | otherwise = Nothing calculate header = Just header{checksum= pure $ sumOf header} fileHeaderRecord :: Format Parser Maybe ByteString cksum -> FileHeader cksum (Format Parser Maybe ByteString) fileHeaderRecord checksumFormat = FileHeader{ fileName = zeroDelimitedUtf8 100, fileMode = zeroDelimitedOctal 8, ownerID = zeroDelimitedOctal 8, groupID = zeroDelimitedOctal 8, fileSize = zeroDelimitedOctal64, modified = zeroDelimitedOctal64, checksum = checksumFormat, fileType = NormalFile <$ value char '0' <|> HardLink <$ value char '1' <|> SymLink <$ value char '2' <|> CharSpecial <$ value char '3' <|> BlockSpecial <$ value char '4' <|> Directory <$ value char '5' <|> FIFO <$ value char '6' <|> ContiguousFile <$ value char '7' <|> ExtendedGlobalHeader <$ value char 'g' <|> ExtendedFileHeader <$ value char 'x' <|> mapValue VendorExtension vendorCode byte <|> mapValue Reserved reservedCode byte, linked = zeroDelimitedUtf8 100, ustar = optional ((value (zeroDelimitedUtf8 8) "ustar " <|> value (zeroDelimitedUtf8 8) "ustar00") *> ustarHeader)} ustarHeader :: Format Parser Maybe ByteString (UStarHeader Identity) ustarHeader = record UStarHeader{ ownerName = zeroDelimitedUtf8 32, groupName = zeroDelimitedUtf8 32, deviceMajor = optionWithDefault (literal $ ByteString.replicate 8 0) (zeroDelimitedOctal 8), deviceMinor = optionWithDefault (literal $ ByteString.replicate 8 0) (zeroDelimitedOctal 8), namePrefix = zeroDelimitedUtf8 167 {- 155+12 to pad to 512 -}} zeroDelimitedUtf8 :: Int -> Format Parser Maybe ByteString Text zeroDelimitedUtf8 width = mapValue decodeUtf8 encodeUtf8 $ ByteString.replicate width 0 `padded1` (takeWhile (/= ByteString.singleton 0)) zeroDelimitedOctal :: Int -> Format Parser Maybe ByteString Word32 zeroDelimitedOctal width = mapValue (fst . head . readOct . ASCII.unpack) (padLeft '0' (width - 1) . ASCII.pack . flip showOct "") $ ByteString.replicate width 0 `padded1` (takeCharsWhile1 isOctDigit) zeroDelimitedOctal64 :: Format Parser Maybe ByteString Word64 zeroDelimitedOctal64 = mapValue (fst . head . readOct . ASCII.unpack) (padLeft '0' 11 . ASCII.pack . flip showOct "") $ ByteString.replicate 12 0 `padded1` (takeCharsWhile1 isOctDigit) padLeft :: Char -> Int -> ByteString -> ByteString padLeft filler len bs | ByteString.length bs < len = ASCII.replicate (len - ByteString.length bs) filler <> bs | otherwise = bs sumOf :: FileHeader () Identity -> Word32 sumOf header = ByteString.foldl' add 0 (fold $ serialize blankFormat header) where add s b = s + fromIntegral b blankFormat = record (fileHeaderRecord $ literal $ ASCII.replicate 8 ' ') instance TextualMonoid ByteString where fromText = encodeUtf8 singleton = ASCII.singleton splitCharacterPrefix = ASCII.uncons dropWhile _ = ASCII.dropWhile dropWhile_ _ = ASCII.dropWhile takeWhile _ = ASCII.takeWhile takeWhile_ _ = ASCII.takeWhile span _ = ASCII.span span_ _ = ASCII.span $(Rank2.TH.deriveAll ''Archive) $(Rank2.TH.deriveAll ''File) $(Rank2.TH.deriveAll ''FileHeader) $(Rank2.TH.deriveAll ''UStarHeader)