tar-0.7.1.0: Codec/Archive/Tar/LongNames.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PackageImports #-}
{-# OPTIONS_HADDOCK hide #-}
module Codec.Archive.Tar.LongNames
( encodeLongNames
, decodeLongNames
, DecodeLongNamesError(..)
) where
import Codec.Archive.Tar.PackAscii
import Codec.Archive.Tar.Types
import Control.Exception
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import "os-string" System.OsString.Posix (PosixString, PosixChar)
import qualified "os-string" System.OsString.Posix as PS
-- | Errors raised by 'decodeLongNames'.
--
-- @since 0.6.0.0
data DecodeLongNamesError
= TwoTypeKEntries
-- ^ Two adjacent 'OtherEntryType' @\'K\'@ nodes.
| TwoTypeLEntries
-- ^ Two adjacent 'OtherEntryType' @\'L\'@ nodes.
| NoLinkEntryAfterTypeKEntry
-- ^ 'OtherEntryType' @\'K\'@ node is not followed by a 'SymbolicLink' / 'HardLink'.
deriving (Eq, Ord, Show)
instance Exception DecodeLongNamesError
-- | Translate high-level entries with POSIX 'FilePath's for files and symlinks
-- into entries suitable for serialization by emitting additional
-- 'OtherEntryType' @\'K\'@ and 'OtherEntryType' @\'L\'@ nodes.
--
-- Input 'FilePath's must be POSIX file names, not native ones.
--
-- @since 0.6.0.0
encodeLongNames
:: GenEntry content FilePath FilePath
-> [GenEntry content TarPath LinkTarget]
encodeLongNames e = maybe id (:) mEntry $ maybe id (:) mEntry' [e'']
where
(mEntry, e') = encodeLinkTarget e
(mEntry', e'') = encodeTarPath e'
encodeTarPath
:: GenEntry content FilePath linkTarget
-> (Maybe (GenEntry content TarPath whatever), GenEntry content TarPath linkTarget)
-- ^ (LongLink entry, actual entry)
encodeTarPath e = case toTarPath' (entryTarPath e) of
FileNameEmpty -> (Nothing, e { entryTarPath = TarPath mempty mempty })
FileNameOK tarPath -> (Nothing, e { entryTarPath = tarPath })
FileNameTooLong tarPath -> (Just $ longLinkEntry $ entryTarPath e, e { entryTarPath = tarPath })
encodeLinkTarget
:: GenEntry content tarPath FilePath
-> (Maybe (GenEntry content TarPath LinkTarget), GenEntry content tarPath LinkTarget)
-- ^ (LongLink symlink entry, actual entry)
encodeLinkTarget e = case entryContent e of
NormalFile x y -> (Nothing, e { entryContent = NormalFile x y })
Directory -> (Nothing, e { entryContent = Directory })
SymbolicLink lnk -> let (mEntry, lnk') = encodeLinkPath lnk in
(mEntry, e { entryContent = SymbolicLink lnk' })
HardLink lnk -> let (mEntry, lnk') = encodeLinkPath lnk in
(mEntry, e { entryContent = HardLink lnk' })
CharacterDevice x y -> (Nothing, e { entryContent = CharacterDevice x y })
BlockDevice x y -> (Nothing, e { entryContent = BlockDevice x y })
NamedPipe -> (Nothing, e { entryContent = NamedPipe })
OtherEntryType x y z -> (Nothing, e { entryContent = OtherEntryType x y z })
encodeLinkPath
:: FilePath
-> (Maybe (GenEntry content TarPath LinkTarget), LinkTarget)
encodeLinkPath lnk = case toTarPath' lnk of
FileNameEmpty -> (Nothing, LinkTarget mempty)
FileNameOK (TarPath name prefix)
| PS.null prefix -> (Nothing, LinkTarget name)
| otherwise -> (Just $ longSymLinkEntry lnk, LinkTarget name)
FileNameTooLong (TarPath name _) ->
(Just $ longSymLinkEntry lnk, LinkTarget name)
-- | Translate low-level entries (usually freshly deserialized) into
-- high-level entries with POSIX 'FilePath's for files and symlinks
-- by parsing and eliminating
-- 'OtherEntryType' @\'K\'@ and 'OtherEntryType' @\'L\'@ nodes.
--
-- Resolved 'FilePath's are still POSIX file names, not native ones.
--
-- @since 0.6.0.0
decodeLongNames
:: Entries e
-> GenEntries BL.ByteString FilePath FilePath (Either e DecodeLongNamesError)
decodeLongNames = go Nothing Nothing
where
isKEntry :: GenEntryContent content linkTarget -> Maybe FilePath
isKEntry = \case
OtherEntryType 'K' fn _ ->
Just $ otherEntryKLPayloadToFilePath fn
OtherEntryType 'x' fn _
| Just fp <- lookup (B.pack "linkpath") (parsePaxExtendedHeader fn) ->
Just $ B.unpack fp
_ -> Nothing
isLEntry :: GenEntryContent content linkTarget -> Maybe FilePath
isLEntry = \case
OtherEntryType 'L' fn _ ->
Just $ otherEntryKLPayloadToFilePath fn
OtherEntryType 'x' fn _
| Just fp <- lookup (B.pack "path") (parsePaxExtendedHeader fn) ->
Just $ B.unpack fp
_ -> Nothing
go :: Maybe FilePath -> Maybe FilePath -> Entries e -> GenEntries BL.ByteString FilePath FilePath (Either e DecodeLongNamesError)
go _ _ (Fail err) = Fail (Left err)
go _ _ Done = Done
go Nothing Nothing (Next e rest)
| Just link <- isKEntry (entryContent e)
= go (Just link) Nothing rest
| Just path <- isLEntry (entryContent e)
= go Nothing (Just path) rest
| otherwise
= Next (castEntry e) (go Nothing Nothing rest)
go Nothing (Just path) (Next e rest)
| Just link <- isKEntry (entryContent e)
= go (Just link) (Just path) rest
| Just{} <- isLEntry (entryContent e)
= Fail $ Right TwoTypeLEntries
| otherwise
= Next ((castEntry e) { entryTarPath = path }) (go Nothing Nothing rest)
go (Just link) Nothing (Next e rest)
| Just{} <- isKEntry (entryContent e)
= Fail $ Right TwoTypeKEntries
| Just path <- isLEntry (entryContent e)
= go (Just link) (Just path) rest
| otherwise
= case entryContent e of
SymbolicLink{} ->
Next ((castEntry e) { entryContent = SymbolicLink link }) (go Nothing Nothing rest)
HardLink{} ->
Next ((castEntry e) { entryContent = HardLink link }) (go Nothing Nothing rest)
_ ->
Fail $ Right NoLinkEntryAfterTypeKEntry
go (Just link) (Just path) (Next e rest)
| Just{} <- isKEntry (entryContent e)
= Fail $ Right TwoTypeKEntries
| Just{} <- isLEntry (entryContent e)
= Fail $ Right TwoTypeLEntries
| otherwise
= case entryContent e of
SymbolicLink{} ->
Next ((castEntry e) { entryTarPath = path, entryContent = SymbolicLink link }) (go Nothing Nothing rest)
HardLink{} ->
Next ((castEntry e) { entryTarPath = path, entryContent = HardLink link }) (go Nothing Nothing rest)
_ ->
Fail $ Right NoLinkEntryAfterTypeKEntry
otherEntryKLPayloadToFilePath :: BL.ByteString -> FilePath
otherEntryKLPayloadToFilePath =
fromPosixString . byteToPosixString . B.takeWhile (/= '\0') . BL.toStrict
parsePaxExtendedHeader :: BL.ByteString -> [(B.ByteString, B.ByteString)]
parsePaxExtendedHeader xs
| BL.null xs = []
| otherwise = case BL.readInt xs of
Nothing -> corrupted
Just (n, ys) -> let (zs, xs') = BL.splitAt (fromIntegral (n - length (show n))) ys in
case B.uncons (BL.toStrict zs) of
Just (' ', us) -> case B.unsnoc us of
Just (vs, '\n') -> let (key, value') = B.span (/= '=') vs in
case B.uncons value' of
Just ('=', value) -> (key, value) : parsePaxExtendedHeader xs'
_ -> corrupted
_ -> corrupted
_ -> corrupted
where
corrupted = [] -- let's be permissive
castEntry :: Entry -> GenEntry BL.ByteString FilePath FilePath
castEntry e = e
{ entryTarPath = fromTarPathToPosixPath (entryTarPath e)
, entryContent = castEntryContent (entryContent e)
}
castEntryContent :: EntryContent -> GenEntryContent BL.ByteString FilePath
castEntryContent = \case
NormalFile x y -> NormalFile x y
Directory -> Directory
SymbolicLink linkTarget -> SymbolicLink $ fromLinkTargetToPosixPath linkTarget
HardLink linkTarget -> HardLink $ fromLinkTargetToPosixPath linkTarget
CharacterDevice x y -> CharacterDevice x y
BlockDevice x y -> BlockDevice x y
NamedPipe -> NamedPipe
OtherEntryType x y z -> OtherEntryType x y z