Blame Codec/Archive/Tar/Types.hs

Packit 8cecbd
{-# LANGUAGE CPP, GeneralizedNewtypeDeriving, BangPatterns #-}
Packit 8cecbd
-----------------------------------------------------------------------------
Packit 8cecbd
-- |
Packit 8cecbd
-- Module      :  Codec.Archive.Tar.Types
Packit 8cecbd
-- Copyright   :  (c) 2007 Bjorn Bringert,
Packit 8cecbd
--                    2008 Andrea Vezzosi,
Packit 8cecbd
--                    2008-2009 Duncan Coutts
Packit 8cecbd
--                    2011 Max Bolingbroke
Packit 8cecbd
-- License     :  BSD3
Packit 8cecbd
--
Packit 8cecbd
-- Maintainer  :  duncan@community.haskell.org
Packit 8cecbd
-- Portability :  portable
Packit 8cecbd
--
Packit 8cecbd
-- Types to represent the content of @.tar@ archives.
Packit 8cecbd
--
Packit 8cecbd
-----------------------------------------------------------------------------
Packit 8cecbd
module Codec.Archive.Tar.Types (
Packit 8cecbd
Packit 8cecbd
  Entry(..),
Packit 8cecbd
  entryPath,
Packit 8cecbd
  EntryContent(..),
Packit 8cecbd
  FileSize,
Packit 8cecbd
  Permissions,
Packit 8cecbd
  Ownership(..),
Packit 8cecbd
  EpochTime,
Packit 8cecbd
  TypeCode,
Packit 8cecbd
  DevMajor,
Packit 8cecbd
  DevMinor,
Packit 8cecbd
  Format(..),
Packit 8cecbd
Packit 8cecbd
  simpleEntry,
Packit 8cecbd
  fileEntry,
Packit 8cecbd
  directoryEntry,
Packit 8cecbd
Packit 8cecbd
  ordinaryFilePermissions,
Packit 8cecbd
  executableFilePermissions,
Packit 8cecbd
  directoryPermissions,
Packit 8cecbd
Packit 8cecbd
  TarPath(..),
Packit 8cecbd
  toTarPath,
Packit 8cecbd
  fromTarPath,
Packit 8cecbd
  fromTarPathToPosixPath,
Packit 8cecbd
  fromTarPathToWindowsPath,
Packit 8cecbd
Packit 8cecbd
  LinkTarget(..),
Packit 8cecbd
  toLinkTarget,
Packit 8cecbd
  fromLinkTarget,
Packit 8cecbd
  fromLinkTargetToPosixPath,
Packit 8cecbd
  fromLinkTargetToWindowsPath,
Packit 8cecbd
Packit 8cecbd
  Entries(..),
Packit 8cecbd
  mapEntries,
Packit 8cecbd
  mapEntriesNoFail,
Packit 8cecbd
  foldEntries,
Packit 8cecbd
  foldlEntries,
Packit 8cecbd
  unfoldEntries,
Packit 8cecbd
Packit 8cecbd
#ifdef TESTS
Packit 8cecbd
  limitToV7FormatCompat
Packit 8cecbd
#endif
Packit 8cecbd
  ) where
Packit 8cecbd
Packit 8cecbd
import Data.Int      (Int64)
Packit 8cecbd
import Data.Monoid   (Monoid(..))
Packit 8cecbd
import qualified Data.ByteString       as BS
Packit 8cecbd
import qualified Data.ByteString.Char8 as BS.Char8
Packit 8cecbd
import qualified Data.ByteString.Lazy  as LBS
Packit 8cecbd
import Control.DeepSeq
Packit 8cecbd
Packit 8cecbd
import qualified System.FilePath as FilePath.Native
Packit 8cecbd
         ( joinPath, splitDirectories, addTrailingPathSeparator )
Packit 8cecbd
import qualified System.FilePath.Posix as FilePath.Posix
Packit 8cecbd
         ( joinPath, splitPath, splitDirectories, hasTrailingPathSeparator
Packit 8cecbd
         , addTrailingPathSeparator )
Packit 8cecbd
import qualified System.FilePath.Windows as FilePath.Windows
Packit 8cecbd
         ( joinPath, addTrailingPathSeparator )
Packit 8cecbd
import System.Posix.Types
Packit 8cecbd
         ( FileMode )
Packit 8cecbd
Packit 8cecbd
#ifdef TESTS
Packit 8cecbd
import Test.QuickCheck
Packit 8cecbd
import Control.Applicative ((<$>), pure, (<*>))
Packit 8cecbd
#endif
Packit 8cecbd
Packit 8cecbd
Packit 8cecbd
type FileSize  = Int64
Packit 8cecbd
-- | The number of seconds since the UNIX epoch
Packit 8cecbd
type EpochTime = Int64
Packit 8cecbd
type DevMajor  = Int
Packit 8cecbd
type DevMinor  = Int
Packit 8cecbd
type TypeCode  = Char
Packit 8cecbd
type Permissions = FileMode
Packit 8cecbd
Packit 8cecbd
-- | Tar archive entry.
Packit 8cecbd
--
Packit 8cecbd
data Entry = Entry {
Packit 8cecbd
Packit 8cecbd
    -- | The path of the file or directory within the archive. This is in a
Packit 8cecbd
    -- tar-specific form. Use 'entryPath' to get a native 'FilePath'.
Packit 8cecbd
    entryTarPath :: {-# UNPACK #-} !TarPath,
Packit 8cecbd
Packit 8cecbd
    -- | The real content of the entry. For 'NormalFile' this includes the
Packit 8cecbd
    -- file data. An entry usually contains a 'NormalFile' or a 'Directory'.
Packit 8cecbd
    entryContent :: !EntryContent,
Packit 8cecbd
Packit 8cecbd
    -- | File permissions (Unix style file mode).
Packit 8cecbd
    entryPermissions :: {-# UNPACK #-} !Permissions,
Packit 8cecbd
Packit 8cecbd
    -- | The user and group to which this file belongs.
Packit 8cecbd
    entryOwnership :: {-# UNPACK #-} !Ownership,
Packit 8cecbd
Packit 8cecbd
    -- | The time the file was last modified.
Packit 8cecbd
    entryTime :: {-# UNPACK #-} !EpochTime,
Packit 8cecbd
Packit 8cecbd
    -- | The tar format the archive is using.
Packit 8cecbd
    entryFormat :: !Format
Packit 8cecbd
  }
Packit 8cecbd
  deriving (Eq, Show)
Packit 8cecbd
Packit 8cecbd
-- | Native 'FilePath' of the file or directory within the archive.
Packit 8cecbd
--
Packit 8cecbd
entryPath :: Entry -> FilePath
Packit 8cecbd
entryPath = fromTarPath . entryTarPath
Packit 8cecbd
Packit 8cecbd
-- | The content of a tar archive entry, which depends on the type of entry.
Packit 8cecbd
--
Packit 8cecbd
-- Portable archives should contain only 'NormalFile' and 'Directory'.
Packit 8cecbd
--
Packit 8cecbd
data EntryContent = NormalFile      LBS.ByteString {-# UNPACK #-} !FileSize
Packit 8cecbd
                  | Directory
Packit 8cecbd
                  | SymbolicLink    !LinkTarget
Packit 8cecbd
                  | HardLink        !LinkTarget
Packit 8cecbd
                  | CharacterDevice {-# UNPACK #-} !DevMajor
Packit 8cecbd
                                    {-# UNPACK #-} !DevMinor
Packit 8cecbd
                  | BlockDevice     {-# UNPACK #-} !DevMajor
Packit 8cecbd
                                    {-# UNPACK #-} !DevMinor
Packit 8cecbd
                  | NamedPipe
Packit 8cecbd
                  | OtherEntryType  {-# UNPACK #-} !TypeCode LBS.ByteString
Packit 8cecbd
                                    {-# UNPACK #-} !FileSize
Packit 8cecbd
    deriving (Eq, Ord, Show)
Packit 8cecbd
Packit 8cecbd
data Ownership = Ownership {
Packit 8cecbd
    -- | The owner user name. Should be set to @\"\"@ if unknown.
Packit 8cecbd
    ownerName :: String,
Packit 8cecbd
Packit 8cecbd
    -- | The owner group name. Should be set to @\"\"@ if unknown.
Packit 8cecbd
    groupName :: String,
Packit 8cecbd
Packit 8cecbd
    -- | Numeric owner user id. Should be set to @0@ if unknown.
Packit 8cecbd
    ownerId :: {-# UNPACK #-} !Int,
Packit 8cecbd
Packit 8cecbd
    -- | Numeric owner group id. Should be set to @0@ if unknown.
Packit 8cecbd
    groupId :: {-# UNPACK #-} !Int
Packit 8cecbd
  }
Packit 8cecbd
    deriving (Eq, Ord, Show)
Packit 8cecbd
Packit 8cecbd
-- | There have been a number of extensions to the tar file format over the
Packit 8cecbd
-- years. They all share the basic entry fields and put more meta-data in
Packit 8cecbd
-- different extended headers.
Packit 8cecbd
--
Packit 8cecbd
data Format =
Packit 8cecbd
Packit 8cecbd
     -- | This is the classic Unix V7 tar format. It does not support owner and
Packit 8cecbd
     -- group names, just numeric Ids. It also does not support device numbers.
Packit 8cecbd
     V7Format
Packit 8cecbd
Packit 8cecbd
     -- | The \"USTAR\" format is an extension of the classic V7 format. It was
Packit 8cecbd
     -- later standardised by POSIX. It has some restrictions but is the most
Packit 8cecbd
     -- portable format.
Packit 8cecbd
     --
Packit 8cecbd
   | UstarFormat
Packit 8cecbd
Packit 8cecbd
     -- | The GNU tar implementation also extends the classic V7 format, though
Packit 8cecbd
     -- in a slightly different way from the USTAR format. In general for new
Packit 8cecbd
     -- archives the standard USTAR/POSIX should be used.
Packit 8cecbd
     --
Packit 8cecbd
   | GnuFormat
Packit 8cecbd
  deriving (Eq, Ord, Show)
Packit 8cecbd
Packit 8cecbd
instance NFData Entry where
Packit 8cecbd
  rnf (Entry _ c _ _ _ _) = rnf c
Packit 8cecbd
Packit 8cecbd
instance NFData EntryContent where
Packit 8cecbd
  rnf x = case x of
Packit 8cecbd
      NormalFile       c _  -> rnflbs c
Packit 8cecbd
      OtherEntryType _ c _  -> rnflbs c
Packit 8cecbd
      _                     -> seq x ()
Packit 8cecbd
    where
Packit 8cecbd
#if MIN_VERSION_bytestring(0,10,0)
Packit 8cecbd
      rnflbs = rnf
Packit 8cecbd
#else
Packit 8cecbd
      rnflbs = foldr (\ !_bs r -> r) () . LBS.toChunks
Packit 8cecbd
#endif
Packit 8cecbd
Packit 8cecbd
instance NFData Ownership where
Packit 8cecbd
  rnf (Ownership o g _ _) = rnf o `seq` rnf g
Packit 8cecbd
Packit 8cecbd
-- | @rw-r--r--@ for normal files
Packit 8cecbd
ordinaryFilePermissions :: Permissions
Packit 8cecbd
ordinaryFilePermissions   = 0o0644
Packit 8cecbd
Packit 8cecbd
-- | @rwxr-xr-x@ for executable files
Packit 8cecbd
executableFilePermissions :: Permissions
Packit 8cecbd
executableFilePermissions = 0o0755
Packit 8cecbd
Packit 8cecbd
-- | @rwxr-xr-x@ for directories
Packit 8cecbd
directoryPermissions :: Permissions
Packit 8cecbd
directoryPermissions  = 0o0755
Packit 8cecbd
Packit 8cecbd
-- | An 'Entry' with all default values except for the file name and type. It
Packit 8cecbd
-- uses the portable USTAR/POSIX format (see 'UstarHeader').
Packit 8cecbd
--
Packit 8cecbd
-- You can use this as a basis and override specific fields, eg:
Packit 8cecbd
--
Packit 8cecbd
-- > (emptyEntry name HardLink) { linkTarget = target }
Packit 8cecbd
--
Packit 8cecbd
simpleEntry :: TarPath -> EntryContent -> Entry
Packit 8cecbd
simpleEntry tarpath content = Entry {
Packit 8cecbd
    entryTarPath     = tarpath,
Packit 8cecbd
    entryContent     = content,
Packit 8cecbd
    entryPermissions = case content of
Packit 8cecbd
                         Directory -> directoryPermissions
Packit 8cecbd
                         _         -> ordinaryFilePermissions,
Packit 8cecbd
    entryOwnership   = Ownership "" "" 0 0,
Packit 8cecbd
    entryTime        = 0,
Packit 8cecbd
    entryFormat      = UstarFormat
Packit 8cecbd
  }
Packit 8cecbd
Packit 8cecbd
-- | A tar 'Entry' for a file.
Packit 8cecbd
--
Packit 8cecbd
-- Entry  fields such as file permissions and ownership have default values.
Packit 8cecbd
--
Packit 8cecbd
-- You can use this as a basis and override specific fields. For example if you
Packit 8cecbd
-- need an executable file you could use:
Packit 8cecbd
--
Packit 8cecbd
-- > (fileEntry name content) { fileMode = executableFileMode }
Packit 8cecbd
--
Packit 8cecbd
fileEntry :: TarPath -> LBS.ByteString -> Entry
Packit 8cecbd
fileEntry name fileContent =
Packit 8cecbd
  simpleEntry name (NormalFile fileContent (LBS.length fileContent))
Packit 8cecbd
Packit 8cecbd
-- | A tar 'Entry' for a directory.
Packit 8cecbd
--
Packit 8cecbd
-- Entry fields such as file permissions and ownership have default values.
Packit 8cecbd
--
Packit 8cecbd
directoryEntry :: TarPath -> Entry
Packit 8cecbd
directoryEntry name = simpleEntry name Directory
Packit 8cecbd
Packit 8cecbd
--
Packit 8cecbd
-- * Tar paths
Packit 8cecbd
--
Packit 8cecbd
Packit 8cecbd
-- | The classic tar format allowed just 100 characters for the file name. The
Packit 8cecbd
-- USTAR format extended this with an extra 155 characters, however it uses a
Packit 8cecbd
-- complex method of splitting the name between the two sections.
Packit 8cecbd
--
Packit 8cecbd
-- Instead of just putting any overflow into the extended area, it uses the
Packit 8cecbd
-- extended area as a prefix. The aggravating insane bit however is that the
Packit 8cecbd
-- prefix (if any) must only contain a directory prefix. That is the split
Packit 8cecbd
-- between the two areas must be on a directory separator boundary. So there is
Packit 8cecbd
-- no simple calculation to work out if a file name is too long. Instead we
Packit 8cecbd
-- have to try to find a valid split that makes the name fit in the two areas.
Packit 8cecbd
--
Packit 8cecbd
-- The rationale presumably was to make it a bit more compatible with old tar
Packit 8cecbd
-- programs that only understand the classic format. A classic tar would be
Packit 8cecbd
-- able to extract the file name and possibly some dir prefix, but not the
Packit 8cecbd
-- full dir prefix. So the files would end up in the wrong place, but that's
Packit 8cecbd
-- probably better than ending up with the wrong names too.
Packit 8cecbd
--
Packit 8cecbd
-- So it's understandable but rather annoying.
Packit 8cecbd
--
Packit 8cecbd
-- * Tar paths use Posix format (ie @\'/\'@ directory separators), irrespective
Packit 8cecbd
--   of the local path conventions.
Packit 8cecbd
--
Packit 8cecbd
-- * The directory separator between the prefix and name is /not/ stored.
Packit 8cecbd
--
Packit 8cecbd
data TarPath = TarPath {-# UNPACK #-} !BS.ByteString -- path name, 100 characters max.
Packit 8cecbd
                       {-# UNPACK #-} !BS.ByteString -- path prefix, 155 characters max.
Packit 8cecbd
  deriving (Eq, Ord)
Packit 8cecbd
Packit 8cecbd
instance NFData TarPath where
Packit 8cecbd
  rnf (TarPath _ _) = () -- fully strict by construction
Packit 8cecbd
Packit 8cecbd
instance Show TarPath where
Packit 8cecbd
  show = show . fromTarPath
Packit 8cecbd
Packit 8cecbd
-- | Convert a 'TarPath' to a native 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
-- The native 'FilePath' will use the native directory separator but it is not
Packit 8cecbd
-- otherwise checked for validity or sanity. In particular:
Packit 8cecbd
--
Packit 8cecbd
-- * The tar path may be invalid as a native path, eg the file name @\"nul\"@
Packit 8cecbd
--   is not valid on Windows.
Packit 8cecbd
--
Packit 8cecbd
-- * The tar path may be an absolute path or may contain @\"..\"@ components.
Packit 8cecbd
--   For security reasons this should not usually be allowed, but it is your
Packit 8cecbd
--   responsibility to check for these conditions (eg using 'checkSecurity').
Packit 8cecbd
--
Packit 8cecbd
fromTarPath :: TarPath -> FilePath
Packit 8cecbd
fromTarPath (TarPath namebs prefixbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Native.joinPath $ FilePath.Posix.splitDirectories prefix
Packit 8cecbd
                          ++ FilePath.Posix.splitDirectories name
Packit 8cecbd
  where
Packit 8cecbd
    name   = BS.Char8.unpack namebs
Packit 8cecbd
    prefix = BS.Char8.unpack prefixbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator name
Packit 8cecbd
                    = FilePath.Native.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Convert a 'TarPath' to a Unix\/Posix 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
-- The difference compared to 'fromTarPath' is that it always returns a Unix
Packit 8cecbd
-- style path irrespective of the current operating system.
Packit 8cecbd
--
Packit 8cecbd
-- This is useful to check how a 'TarPath' would be interpreted on a specific
Packit 8cecbd
-- operating system, eg to perform portability checks.
Packit 8cecbd
--
Packit 8cecbd
fromTarPathToPosixPath :: TarPath -> FilePath
Packit 8cecbd
fromTarPathToPosixPath (TarPath namebs prefixbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Posix.joinPath $ FilePath.Posix.splitDirectories prefix
Packit 8cecbd
                         ++ FilePath.Posix.splitDirectories name
Packit 8cecbd
  where
Packit 8cecbd
    name   = BS.Char8.unpack namebs
Packit 8cecbd
    prefix = BS.Char8.unpack prefixbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator name
Packit 8cecbd
                    = FilePath.Posix.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Convert a 'TarPath' to a Windows 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
-- The only difference compared to 'fromTarPath' is that it always returns a
Packit 8cecbd
-- Windows style path irrespective of the current operating system.
Packit 8cecbd
--
Packit 8cecbd
-- This is useful to check how a 'TarPath' would be interpreted on a specific
Packit 8cecbd
-- operating system, eg to perform portability checks.
Packit 8cecbd
--
Packit 8cecbd
fromTarPathToWindowsPath :: TarPath -> FilePath
Packit 8cecbd
fromTarPathToWindowsPath (TarPath namebs prefixbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Windows.joinPath $ FilePath.Posix.splitDirectories prefix
Packit 8cecbd
                           ++ FilePath.Posix.splitDirectories name
Packit 8cecbd
  where
Packit 8cecbd
    name   = BS.Char8.unpack namebs
Packit 8cecbd
    prefix = BS.Char8.unpack prefixbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator name
Packit 8cecbd
                    = FilePath.Windows.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Convert a native 'FilePath' to a 'TarPath'.
Packit 8cecbd
--
Packit 8cecbd
-- The conversion may fail if the 'FilePath' is too long. See 'TarPath' for a
Packit 8cecbd
-- description of the problem with splitting long 'FilePath's.
Packit 8cecbd
--
Packit 8cecbd
toTarPath :: Bool -- ^ Is the path for a directory? This is needed because for
Packit 8cecbd
                  -- directories a 'TarPath' must always use a trailing @\/@.
Packit 8cecbd
          -> FilePath -> Either String TarPath
Packit 8cecbd
toTarPath isDir = splitLongPath
Packit 8cecbd
                . addTrailingSep
Packit 8cecbd
                . FilePath.Posix.joinPath
Packit 8cecbd
                . FilePath.Native.splitDirectories
Packit 8cecbd
  where
Packit 8cecbd
    addTrailingSep | isDir     = FilePath.Posix.addTrailingPathSeparator
Packit 8cecbd
                   | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Take a sanitised path, split on directory separators and try to pack it
Packit 8cecbd
-- into the 155 + 100 tar file name format.
Packit 8cecbd
--
Packit 8cecbd
-- The strategy is this: take the name-directory components in reverse order
Packit 8cecbd
-- and try to fit as many components into the 100 long name area as possible.
Packit 8cecbd
-- If all the remaining components fit in the 155 name area then we win.
Packit 8cecbd
--
Packit 8cecbd
splitLongPath :: FilePath -> Either String TarPath
Packit 8cecbd
splitLongPath path =
Packit 8cecbd
  case packName nameMax (reverse (FilePath.Posix.splitPath path)) of
Packit 8cecbd
    Left err                 -> Left err
Packit 8cecbd
    Right (name, [])         -> Right $! TarPath (BS.Char8.pack name)
Packit 8cecbd
                                                  BS.empty
Packit 8cecbd
    Right (name, first:rest) -> case packName prefixMax remainder of
Packit 8cecbd
      Left err               -> Left err
Packit 8cecbd
      Right (_     , (_:_))  -> Left "File name too long (cannot split)"
Packit 8cecbd
      Right (prefix, [])     -> Right $! TarPath (BS.Char8.pack name)
Packit 8cecbd
                                                 (BS.Char8.pack prefix)
Packit 8cecbd
      where
Packit 8cecbd
        -- drop the '/' between the name and prefix:
Packit 8cecbd
        remainder = init first : rest
Packit 8cecbd
Packit 8cecbd
  where
Packit 8cecbd
    nameMax, prefixMax :: Int
Packit 8cecbd
    nameMax   = 100
Packit 8cecbd
    prefixMax = 155
Packit 8cecbd
Packit 8cecbd
    packName _      []     = Left "File name empty"
Packit 8cecbd
    packName maxLen (c:cs)
Packit 8cecbd
      | n > maxLen         = Left "File name too long"
Packit 8cecbd
      | otherwise          = Right (packName' maxLen n [c] cs)
Packit 8cecbd
      where n = length c
Packit 8cecbd
Packit 8cecbd
    packName' maxLen n ok (c:cs)
Packit 8cecbd
      | n' <= maxLen             = packName' maxLen n' (c:ok) cs
Packit 8cecbd
                                     where n' = n + length c
Packit 8cecbd
    packName' _      _ ok    cs  = (FilePath.Posix.joinPath ok, cs)
Packit 8cecbd
Packit 8cecbd
-- | The tar format allows just 100 ASCII characters for the 'SymbolicLink' and
Packit 8cecbd
-- 'HardLink' entry types.
Packit 8cecbd
--
Packit 8cecbd
newtype LinkTarget = LinkTarget BS.ByteString
Packit 8cecbd
  deriving (Eq, Ord, Show)
Packit 8cecbd
Packit 8cecbd
instance NFData LinkTarget where
Packit 8cecbd
#if MIN_VERSION_bytestring(0,10,0)
Packit 8cecbd
    rnf (LinkTarget bs) = rnf bs
Packit 8cecbd
#else
Packit 8cecbd
    rnf (LinkTarget !_bs) = ()
Packit 8cecbd
#endif
Packit 8cecbd
Packit 8cecbd
-- | Convert a native 'FilePath' to a tar 'LinkTarget'. This may fail if the
Packit 8cecbd
-- string is longer than 100 characters or if it contains non-portable
Packit 8cecbd
-- characters.
Packit 8cecbd
--
Packit 8cecbd
toLinkTarget   :: FilePath -> Maybe LinkTarget
Packit 8cecbd
toLinkTarget path | length path <= 100 = Just $! LinkTarget (BS.Char8.pack path)
Packit 8cecbd
                  | otherwise          = Nothing
Packit 8cecbd
Packit 8cecbd
-- | Convert a tar 'LinkTarget' to a native 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
fromLinkTarget :: LinkTarget -> FilePath
Packit 8cecbd
fromLinkTarget (LinkTarget pathbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Native.joinPath $ FilePath.Posix.splitDirectories path
Packit 8cecbd
  where
Packit 8cecbd
    path = BS.Char8.unpack pathbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator path
Packit 8cecbd
                    = FilePath.Native.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Convert a tar 'LinkTarget' to a Unix/Posix 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
fromLinkTargetToPosixPath :: LinkTarget -> FilePath
Packit 8cecbd
fromLinkTargetToPosixPath (LinkTarget pathbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Posix.joinPath $ FilePath.Posix.splitDirectories path
Packit 8cecbd
  where
Packit 8cecbd
    path = BS.Char8.unpack pathbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator path
Packit 8cecbd
                    = FilePath.Native.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
-- | Convert a tar 'LinkTarget' to a Windows 'FilePath'.
Packit 8cecbd
--
Packit 8cecbd
fromLinkTargetToWindowsPath :: LinkTarget -> FilePath
Packit 8cecbd
fromLinkTargetToWindowsPath (LinkTarget pathbs) = adjustDirectory $
Packit 8cecbd
  FilePath.Windows.joinPath $ FilePath.Posix.splitDirectories path
Packit 8cecbd
  where
Packit 8cecbd
    path = BS.Char8.unpack pathbs
Packit 8cecbd
    adjustDirectory | FilePath.Posix.hasTrailingPathSeparator path
Packit 8cecbd
                    = FilePath.Windows.addTrailingPathSeparator
Packit 8cecbd
                    | otherwise = id
Packit 8cecbd
Packit 8cecbd
--
Packit 8cecbd
-- * Entries type
Packit 8cecbd
--
Packit 8cecbd
Packit 8cecbd
-- | A tar archive is a sequence of entries.
Packit 8cecbd
--
Packit 8cecbd
-- The point of this type as opposed to just using a list is that it makes the
Packit 8cecbd
-- failure case explicit. We need this because the sequence of entries we get
Packit 8cecbd
-- from reading a tarball can include errors.
Packit 8cecbd
--
Packit 8cecbd
-- It is a concrete data type so you can manipulate it directly but it is often
Packit 8cecbd
-- clearer to use the provided functions for mapping, folding and unfolding.
Packit 8cecbd
--
Packit 8cecbd
-- Converting from a list can be done with just @foldr Next Done@. Converting
Packit 8cecbd
-- back into a list can be done with 'foldEntries' however in that case you
Packit 8cecbd
-- must be prepared to handle the 'Fail' case inherent in the 'Entries' type.
Packit 8cecbd
--
Packit 8cecbd
-- The 'Monoid' instance lets you concatenate archives or append entries to an
Packit 8cecbd
-- archive.
Packit 8cecbd
--
Packit 8cecbd
data Entries e = Next Entry (Entries e)
Packit 8cecbd
               | Done
Packit 8cecbd
               | Fail e
Packit 8cecbd
  deriving (Eq, Show)
Packit 8cecbd
Packit 8cecbd
infixr 5 `Next`
Packit 8cecbd
Packit 8cecbd
-- | This is like the standard 'unfoldr' function on lists, but for 'Entries'.
Packit 8cecbd
-- It includes failure as an extra possibility that the stepper function may
Packit 8cecbd
-- return.
Packit 8cecbd
--
Packit 8cecbd
-- It can be used to generate 'Entries' from some other type. For example it is
Packit 8cecbd
-- used internally to lazily unfold entries from a 'LBS.ByteString'.
Packit 8cecbd
--
Packit 8cecbd
unfoldEntries :: (a -> Either e (Maybe (Entry, a))) -> a -> Entries e
Packit 8cecbd
unfoldEntries f = unfold
Packit 8cecbd
  where
Packit 8cecbd
    unfold x = case f x of
Packit 8cecbd
      Left err             -> Fail err
Packit 8cecbd
      Right Nothing        -> Done
Packit 8cecbd
      Right (Just (e, x')) -> Next e (unfold x')
Packit 8cecbd
Packit 8cecbd
-- | This is like the standard 'foldr' function on lists, but for 'Entries'.
Packit 8cecbd
-- Compared to 'foldr' it takes an extra function to account for the
Packit 8cecbd
-- possibility of failure.
Packit 8cecbd
--
Packit 8cecbd
-- This is used to consume a sequence of entries. For example it could be used
Packit 8cecbd
-- to scan a tarball for problems or to collect an index of the contents.
Packit 8cecbd
--
Packit 8cecbd
foldEntries :: (Entry -> a -> a) -> a -> (e -> a) -> Entries e -> a
Packit 8cecbd
foldEntries next done fail' = fold
Packit 8cecbd
  where
Packit 8cecbd
    fold (Next e es) = next e (fold es)
Packit 8cecbd
    fold Done        = done
Packit 8cecbd
    fold (Fail err)  = fail' err
Packit 8cecbd
Packit 8cecbd
-- | A 'foldl'-like function on Entries. It either returns the final
Packit 8cecbd
-- accumulator result, or the failure along with the intermediate accumulator
Packit 8cecbd
-- value.
Packit 8cecbd
--
Packit 8cecbd
foldlEntries :: (a -> Entry -> a) -> a -> Entries e -> Either (e, a) a
Packit 8cecbd
foldlEntries f z = go z
Packit 8cecbd
  where
Packit 8cecbd
    go !acc (Next e es) = go (f acc e) es
Packit 8cecbd
    go !acc  Done       = Right acc
Packit 8cecbd
    go !acc (Fail err)  = Left (err, acc)
Packit 8cecbd
Packit 8cecbd
-- | This is like the standard 'map' function on lists, but for 'Entries'. It
Packit 8cecbd
-- includes failure as a extra possible outcome of the mapping function.
Packit 8cecbd
--
Packit 8cecbd
-- If your mapping function cannot fail it may be more convenient to use
Packit 8cecbd
-- 'mapEntriesNoFail'
Packit 8cecbd
mapEntries :: (Entry -> Either e' Entry) -> Entries e -> Entries (Either e e')
Packit 8cecbd
mapEntries f =
Packit 8cecbd
  foldEntries (\entry rest -> either (Fail . Right) (flip Next rest) (f entry)) Done (Fail . Left)
Packit 8cecbd
Packit 8cecbd
-- | Like 'mapEntries' but the mapping function itself cannot fail.
Packit 8cecbd
--
Packit 8cecbd
mapEntriesNoFail :: (Entry -> Entry) -> Entries e -> Entries e
Packit 8cecbd
mapEntriesNoFail f =
Packit 8cecbd
  foldEntries (\entry -> Next (f entry)) Done Fail
Packit 8cecbd
Packit 8cecbd
instance Monoid (Entries e) where
Packit 8cecbd
  mempty      = Done
Packit 8cecbd
  mappend a b = foldEntries Next b Fail a
Packit 8cecbd
Packit 8cecbd
instance Functor Entries where
Packit 8cecbd
  fmap f = foldEntries Next Done (Fail . f)
Packit 8cecbd
Packit 8cecbd
instance NFData e => NFData (Entries e) where
Packit 8cecbd
  rnf (Next e es) = rnf e `seq` rnf es
Packit 8cecbd
  rnf  Done       = ()
Packit 8cecbd
  rnf (Fail e)    = rnf e
Packit 8cecbd
Packit 8cecbd
Packit 8cecbd
-------------------------
Packit 8cecbd
-- QuickCheck instances
Packit 8cecbd
--
Packit 8cecbd
Packit 8cecbd
#ifdef TESTS
Packit 8cecbd
Packit 8cecbd
instance Arbitrary Entry where
Packit 8cecbd
  arbitrary = Entry <$> arbitrary <*> arbitrary <*> arbitraryPermissions
Packit 8cecbd
                    <*> arbitrary <*> arbitraryEpochTime <*> arbitrary
Packit 8cecbd
    where
Packit 8cecbd
      arbitraryPermissions :: Gen Permissions
Packit 8cecbd
      arbitraryPermissions = fromIntegral <$> (arbitraryOctal 7 :: Gen Int)
Packit 8cecbd
Packit 8cecbd
      arbitraryEpochTime :: Gen EpochTime
Packit 8cecbd
      arbitraryEpochTime = fromIntegral <$> (arbitraryOctal 11 :: Gen Int)
Packit 8cecbd
Packit 8cecbd
  shrink (Entry path content perms author time format) =
Packit 8cecbd
      [ Entry path' content' perms author' time' format 
Packit 8cecbd
      | (path', content', author', time') <-
Packit 8cecbd
         shrink (path, content, author, time) ]
Packit 8cecbd
   ++ [ Entry path content perms' author time format
Packit 8cecbd
      | perms' <- shrinkIntegral perms ]
Packit 8cecbd
Packit 8cecbd
instance Arbitrary TarPath where
Packit 8cecbd
  arbitrary = either error id
Packit 8cecbd
            . toTarPath False
Packit 8cecbd
            . FilePath.Posix.joinPath
Packit 8cecbd
          <$> listOf1ToN (255 `div` 5)
Packit 8cecbd
                         (elements (map (replicate 4) "abcd"))
Packit 8cecbd
Packit 8cecbd
  shrink = map (either error id . toTarPath False)
Packit 8cecbd
         . map FilePath.Posix.joinPath
Packit 8cecbd
         . filter (not . null)
Packit 8cecbd
         . shrinkList shrinkNothing
Packit 8cecbd
         . FilePath.Posix.splitPath
Packit 8cecbd
         . fromTarPathToPosixPath
Packit 8cecbd
Packit 8cecbd
instance Arbitrary LinkTarget where
Packit 8cecbd
  arbitrary = maybe (error "link target too large") id
Packit 8cecbd
            . toLinkTarget
Packit 8cecbd
            . FilePath.Native.joinPath
Packit 8cecbd
          <$> listOf1ToN (100 `div` 5)
Packit 8cecbd
                         (elements (map (replicate 4) "abcd"))
Packit 8cecbd
Packit 8cecbd
  shrink = map (maybe (error "link target too large") id . toLinkTarget)
Packit 8cecbd
         . map FilePath.Posix.joinPath
Packit 8cecbd
         . filter (not . null)
Packit 8cecbd
         . shrinkList shrinkNothing
Packit 8cecbd
         . FilePath.Posix.splitPath
Packit 8cecbd
         . fromLinkTargetToPosixPath
Packit 8cecbd
Packit 8cecbd
Packit 8cecbd
listOf1ToN :: Int -> Gen a -> Gen [a]
Packit 8cecbd
listOf1ToN n g = sized $ \sz -> do
Packit 8cecbd
    n <- choose (1, min n (max 1 sz))
Packit 8cecbd
    vectorOf n g
Packit 8cecbd
Packit 8cecbd
listOf0ToN :: Int -> Gen a -> Gen [a]
Packit 8cecbd
listOf0ToN n g = sized $ \sz -> do
Packit 8cecbd
    n <- choose (0, min n sz)
Packit 8cecbd
    vectorOf n g
Packit 8cecbd
Packit 8cecbd
instance Arbitrary EntryContent where
Packit 8cecbd
  arbitrary =
Packit 8cecbd
    frequency
Packit 8cecbd
      [ (16, do bs <- arbitrary;
Packit 8cecbd
                return (NormalFile bs (LBS.length bs)))
Packit 8cecbd
      , (2, pure Directory)
Packit 8cecbd
      , (1, SymbolicLink    <$> arbitrary)
Packit 8cecbd
      , (1, HardLink        <$> arbitrary)
Packit 8cecbd
      , (1, CharacterDevice <$> arbitraryOctal 7 <*> arbitraryOctal 7)
Packit 8cecbd
      , (1, BlockDevice     <$> arbitraryOctal 7 <*> arbitraryOctal 7)
Packit 8cecbd
      , (1, pure NamedPipe)
Packit 8cecbd
      , (1, do c  <- elements (['A'..'Z']++['a'..'z'])
Packit 8cecbd
               bs <- arbitrary;
Packit 8cecbd
               return (OtherEntryType c bs (LBS.length bs)))
Packit 8cecbd
      ]
Packit 8cecbd
Packit 8cecbd
  shrink (NormalFile bs _)   = [ NormalFile bs' (LBS.length bs') 
Packit 8cecbd
                               | bs' <- shrink bs ]
Packit 8cecbd
  shrink  Directory          = []
Packit 8cecbd
  shrink (SymbolicLink link) = [ SymbolicLink link' | link' <- shrink link ]
Packit 8cecbd
  shrink (HardLink     link) = [ HardLink     link' | link' <- shrink link ]
Packit 8cecbd
  shrink (CharacterDevice ma mi) = [ CharacterDevice ma' mi'
Packit 8cecbd
                                   | (ma', mi') <- shrink (ma, mi) ]
Packit 8cecbd
  shrink (BlockDevice     ma mi) = [ BlockDevice ma' mi'
Packit 8cecbd
                                   | (ma', mi') <- shrink (ma, mi) ]
Packit 8cecbd
  shrink  NamedPipe              = []
Packit 8cecbd
  shrink (OtherEntryType c bs _) = [ OtherEntryType c bs' (LBS.length bs') 
Packit 8cecbd
                                   | bs' <- shrink bs ]
Packit 8cecbd
Packit 8cecbd
instance Arbitrary LBS.ByteString where
Packit 8cecbd
  arbitrary = fmap LBS.pack arbitrary
Packit 8cecbd
  shrink    = map LBS.pack . shrink . LBS.unpack
Packit 8cecbd
Packit 8cecbd
instance Arbitrary BS.ByteString where
Packit 8cecbd
  arbitrary = fmap BS.pack arbitrary
Packit 8cecbd
  shrink    = map BS.pack . shrink . BS.unpack
Packit 8cecbd
Packit 8cecbd
instance Arbitrary Ownership where
Packit 8cecbd
  arbitrary = Ownership <$> name <*> name
Packit 8cecbd
                        <*> idno <*> idno
Packit 8cecbd
    where
Packit 8cecbd
      name = listOf0ToN 32 (arbitrary `suchThat` (/= '\0'))
Packit 8cecbd
      idno = arbitraryOctal 7
Packit 8cecbd
Packit 8cecbd
  shrink (Ownership oname gname oid gid) =
Packit 8cecbd
    [ Ownership oname' gname' oid' gid'
Packit 8cecbd
    | (oname', gname', oid', gid') <- shrink (oname, gname, oid, gid) ]
Packit 8cecbd
Packit 8cecbd
instance Arbitrary Format where
Packit 8cecbd
  arbitrary = elements [V7Format, UstarFormat, GnuFormat]
Packit 8cecbd
Packit 8cecbd
Packit 8cecbd
--arbitraryOctal :: (Integral n, Random n) => Int -> Gen n
Packit 8cecbd
arbitraryOctal n =
Packit 8cecbd
    oneof [ pure 0
Packit 8cecbd
          , choose (0, upperBound)
Packit 8cecbd
          , pure upperBound
Packit 8cecbd
          ]
Packit 8cecbd
  where
Packit 8cecbd
    upperBound = 8^n-1
Packit 8cecbd
Packit 8cecbd
-- For QC tests it's useful to have a way to limit the info to that which can
Packit 8cecbd
-- be expressed in the old V7 format
Packit 8cecbd
limitToV7FormatCompat :: Entry -> Entry
Packit 8cecbd
limitToV7FormatCompat entry@Entry { entryFormat = V7Format } =
Packit 8cecbd
    entry {
Packit 8cecbd
      entryContent = case entryContent entry of
Packit 8cecbd
        CharacterDevice _ _ -> OtherEntryType  '3' LBS.empty 0
Packit 8cecbd
        BlockDevice     _ _ -> OtherEntryType  '4' LBS.empty 0
Packit 8cecbd
        Directory           -> OtherEntryType  '5' LBS.empty 0
Packit 8cecbd
        NamedPipe           -> OtherEntryType  '6' LBS.empty 0
Packit 8cecbd
        other               -> other,
Packit 8cecbd
Packit 8cecbd
      entryOwnership = (entryOwnership entry) {
Packit 8cecbd
        groupName = "",
Packit 8cecbd
        ownerName = ""
Packit 8cecbd
      },
Packit 8cecbd
Packit 8cecbd
      entryTarPath = let TarPath name _prefix = entryTarPath entry
Packit 8cecbd
                      in TarPath name BS.empty
Packit 8cecbd
    }
Packit 8cecbd
limitToV7FormatCompat entry = entry
Packit 8cecbd
Packit 8cecbd
#endif
Packit 8cecbd