{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, CPP #-}
module Ar
(ArchiveEntry(..)
,Archive(..)
,afilter
,parseAr
,loadAr
,loadObj
,writeBSDAr
,writeGNUAr
,isBSDSymdef
,isGNUSymdef
)
where
import GhcPrelude
import Data.Semigroup (Semigroup)
import Data.List (mapAccumL, isPrefixOf)
import Data.Monoid ((<>))
import Data.Binary.Get
import Data.Binary.Put
import Control.Monad
import Control.Applicative
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy as L
#if !defined(mingw32_HOST_OS)
import qualified System.Posix.Files as POSIX
#endif
import System.FilePath (takeFileName)
data ArchiveEntry = ArchiveEntry
{ filename :: String
, filetime :: Int
, fileown :: Int
, filegrp :: Int
, filemode :: Int
, filesize :: Int
, filedata :: B.ByteString
} deriving (Eq, Show)
newtype Archive = Archive [ArchiveEntry]
deriving (Eq, Show, Semigroup, Monoid)
afilter :: (ArchiveEntry -> Bool) -> Archive -> Archive
afilter f (Archive xs) = Archive (filter f xs)
isBSDSymdef, isGNUSymdef :: ArchiveEntry -> Bool
isBSDSymdef a = "__.SYMDEF" `isPrefixOf` (filename a)
isGNUSymdef a = "/" == (filename a)
getPaddedInt :: B.ByteString -> Int
getPaddedInt = read . C.unpack . C.takeWhile (/= '\x20')
putPaddedInt :: Int -> Int -> Put
putPaddedInt padding i = putPaddedString '\x20' padding (show i)
putPaddedString :: Char -> Int -> String -> Put
putPaddedString pad padding s = putByteString . C.pack . take padding $ s `mappend` (repeat pad)
getBSDArchEntries :: Get [ArchiveEntry]
getBSDArchEntries = do
empty <- isEmpty
if empty then
return []
else do
name <- getByteString 16
when ('/' `C.elem` name && C.take 3 name /= "#1/") $
fail "Looks like GNU Archive"
time <- getPaddedInt <$> getByteString 12
own <- getPaddedInt <$> getByteString 6
grp <- getPaddedInt <$> getByteString 6
mode <- getPaddedInt <$> getByteString 8
st_size <- getPaddedInt <$> getByteString 10
end <- getByteString 2
when (end /= "\x60\x0a") $
fail "Invalid archive header end marker"
off1 <- liftM fromIntegral bytesRead :: Get Int
name <- if C.unpack (C.take 3 name) == "#1/" then
liftM (C.unpack . C.takeWhile (/= '\0')) (getByteString $ read $ C.unpack $ C.drop 3 name)
else
return $ C.unpack $ C.takeWhile (/= ' ') name
off2 <- liftM fromIntegral bytesRead :: Get Int
file <- getByteString (st_size - (off2 - off1))
rest <- getBSDArchEntries
return $ (ArchiveEntry name time own grp mode (st_size - (off2 - off1)) file) : rest
getGNUArchEntries :: Maybe ArchiveEntry -> Get [ArchiveEntry]
getGNUArchEntries extInfo = do
empty <- isEmpty
if empty
then return []
else
do
name <- getByteString 16
time <- getPaddedInt <$> getByteString 12
own <- getPaddedInt <$> getByteString 6
grp <- getPaddedInt <$> getByteString 6
mode <- getPaddedInt <$> getByteString 8
st_size <- getPaddedInt <$> getByteString 10
end <- getByteString 2
when (end /= "\x60\x0a") $
fail "Invalid archive header end marker"
file <- getByteString st_size
name <- return . C.unpack $
if C.unpack (C.take 1 name) == "/"
then case C.takeWhile (/= ' ') name of
name@"/" -> name
name@"//" -> name
name -> getExtName extInfo (read . C.unpack $ C.drop 1 name)
else C.takeWhile (/= '/') name
case name of
"/" -> getGNUArchEntries extInfo
"//" -> getGNUArchEntries (Just (ArchiveEntry name time own grp mode st_size file))
_ -> (ArchiveEntry name time own grp mode st_size file :) <$> getGNUArchEntries extInfo
where
getExtName :: Maybe ArchiveEntry -> Int -> B.ByteString
getExtName Nothing _ = error "Invalid extended filename reference."
getExtName (Just info) offset = C.takeWhile (/= '/') . C.drop offset $ filedata info
putArchEntry :: ArchiveEntry -> PutM ()
putArchEntry (ArchiveEntry name time own grp mode st_size file) = do
putPaddedString ' ' 16 name
putPaddedInt 12 time
putPaddedInt 6 own
putPaddedInt 6 grp
putPaddedInt 8 mode
putPaddedInt 10 (st_size + pad)
putByteString "\x60\x0a"
putByteString file
when (pad == 1) $
putWord8 0x0a
where
pad = st_size `mod` 2
getArchMagic :: Get ()
getArchMagic = do
magic <- liftM C.unpack $ getByteString 8
if magic /= "!<arch>\n"
then fail $ "Invalid magic number " ++ show magic
else return ()
putArchMagic :: Put
putArchMagic = putByteString $ C.pack "!<arch>\n"
getArch :: Get Archive
getArch = Archive <$> do
getArchMagic
getBSDArchEntries <|> getGNUArchEntries Nothing
putBSDArch :: Archive -> PutM ()
putBSDArch (Archive as) = do
putArchMagic
mapM_ putArchEntry (processEntries as)
where
padStr pad size str = take size $ str <> repeat pad
nameSize name = case length name `divMod` 4 of
(n, 0) -> 4 * n
(n, _) -> 4 * (n + 1)
needExt name = length name > 16 || ' ' `elem` name
processEntry :: ArchiveEntry -> ArchiveEntry
processEntry archive@(ArchiveEntry name _ _ _ _ st_size _)
| needExt name = archive { filename = "#1/" <> show sz
, filedata = C.pack (padStr '\0' sz name) <> filedata archive
, filesize = st_size + sz }
| otherwise = archive
where sz = nameSize name
processEntries = map processEntry
putGNUArch :: Archive -> PutM ()
putGNUArch (Archive as) = do
putArchMagic
mapM_ putArchEntry (processEntries as)
where
processEntry :: ArchiveEntry -> ArchiveEntry -> (ArchiveEntry, ArchiveEntry)
processEntry extInfo archive@(ArchiveEntry name _ _ _ _ _ _)
| length name > 15 = ( extInfo { filesize = filesize extInfo + length name + 2
, filedata = filedata extInfo <> C.pack name <> "/\n" }
, archive { filename = "/" <> show (filesize extInfo) } )
| otherwise = ( extInfo, archive { filename = name <> "/" } )
processEntries :: [ArchiveEntry] -> [ArchiveEntry]
processEntries =
uncurry (:) . mapAccumL processEntry (ArchiveEntry "//" 0 0 0 0 0 mempty)
parseAr :: B.ByteString -> Archive
parseAr = runGet getArch . L.fromChunks . pure
writeBSDAr, writeGNUAr :: FilePath -> Archive -> IO ()
writeBSDAr fp = L.writeFile fp . runPut . putBSDArch
writeGNUAr fp = L.writeFile fp . runPut . putGNUArch
loadAr :: FilePath -> IO Archive
loadAr fp = parseAr <$> B.readFile fp
loadObj :: FilePath -> IO ArchiveEntry
loadObj fp = do
payload <- B.readFile fp
(modt, own, grp, mode) <- fileInfo fp
return $ ArchiveEntry
(takeFileName fp) modt own grp mode
(B.length payload) payload
fileInfo :: FilePath -> IO ( Int, Int, Int, Int)
#if defined(mingw32_HOST_OS)
fileInfo _ = pure (0,0,0,0)
#else
fileInfo fp = go <$> POSIX.getFileStatus fp
where go status = ( fromEnum $ POSIX.modificationTime status
, fromIntegral $ POSIX.fileOwner status
, fromIntegral $ POSIX.fileGroup status
, oct2dec . fromIntegral $ POSIX.fileMode status
)
oct2dec :: Int -> Int
oct2dec = foldl (\a b -> a * 10 + b) 0 . reverse . dec 8
where dec _ 0 = []
dec b i = let (rest, last) = i `quotRem` b
in last:dec b rest
#endif