1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
|
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
-- |
-- Module : Codec.Archive.Zip.Type
-- Copyright : © 2016–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- Types used by the package.
module Codec.Archive.Zip.Type
( -- * Entry selector
EntrySelector,
mkEntrySelector,
unEntrySelector,
getEntryName,
EntrySelectorException (..),
-- * Entry description
EntryDescription (..),
CompressionMethod (..),
-- * Archive description
ArchiveDescription (..),
-- * Exceptions
ZipException (..),
)
where
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow (..))
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
import Data.Data (Data)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Time.Clock (UTCTime)
import Data.Typeable (Typeable)
import Data.Version (Version)
import Data.Word (Word16, Word32)
import Numeric.Natural
import System.FilePath qualified as FP
import System.FilePath.Posix qualified as Posix
import System.FilePath.Windows qualified as Windows
----------------------------------------------------------------------------
-- Entry selector
-- | This data type serves for naming and selection of archive entries. It
-- can be created only with the help of the smart constructor
-- 'mkEntrySelector', and it's the only “key” that can be used to refer to
-- files in the archive or to name new archive entries.
--
-- The abstraction is crucial for ensuring that created archives are
-- portable across operating systems, file systems, and platforms. Since on
-- some operating systems, file paths are case-insensitive, this selector is
-- also case-insensitive. It makes sure that only relative paths are used to
-- name files inside archive, as it's recommended in the specification. It
-- also guarantees that forward slashes are used when the path is stored
-- inside the archive for compatibility with Unix-like operating systems (as
-- recommended in the specification). On the other hand, in can be rendered
-- as an ordinary relative file path in OS-specific format when needed.
newtype EntrySelector = EntrySelector
{ -- | Path pieces of relative path inside archive
unES :: NonEmpty (CI String)
}
deriving (Eq, Ord, Typeable)
instance Show EntrySelector where
show = show . unEntrySelector
-- | Create an 'EntrySelector' from a 'FilePath'. To avoid problems with
-- distribution of the archive, characters that some operating systems do
-- not expect in paths are not allowed.
--
-- Argument to 'mkEntrySelector' should pass these checks:
--
-- * 'System.FilePath.Posix.isValid'
-- * 'System.FilePath.Windows.isValid'
-- * it is a relative path without slash at the end
-- * binary representations of normalized path should be not longer than
-- 65535 bytes
--
-- This function can throw an 'EntrySelectorException'.
mkEntrySelector :: (MonadThrow m) => FilePath -> m EntrySelector
mkEntrySelector path =
let f x =
case filter (not . FP.isPathSeparator) x of
[] -> Nothing
xs -> Just (CI.mk xs)
giveup = throwM (InvalidEntrySelector path)
in case NE.nonEmpty (mapMaybe f (FP.splitPath path)) of
Nothing -> giveup
Just pieces ->
let selector = EntrySelector pieces
binLength = B.length . T.encodeUtf8 . getEntryName
in if Posix.isValid path
&& Windows.isValid path
&& not (FP.isAbsolute path || FP.hasTrailingPathSeparator path)
&& (CI.mk "." `notElem` pieces)
&& (CI.mk ".." `notElem` pieces)
&& binLength selector <= 0xffff
then return selector
else giveup
-- | Restore a relative path from 'EntrySelector'. Every 'EntrySelector'
-- corresponds to a 'FilePath'.
unEntrySelector :: EntrySelector -> FilePath
unEntrySelector =
FP.joinPath . fmap CI.original . NE.toList . unES
-- | Get an entry name in the from that is suitable for writing to file
-- header, given an 'EntrySelector'.
getEntryName :: EntrySelector -> Text
getEntryName =
T.pack . concat . NE.toList . NE.intersperse "/" . fmap CI.original . unES
-- | The problems you can have with an 'EntrySelector'.
newtype EntrySelectorException
= -- | 'EntrySelector' cannot be created from this path
InvalidEntrySelector FilePath
deriving (Eq, Ord, Typeable)
instance Show EntrySelectorException where
show (InvalidEntrySelector path) = "Cannot build selector from " ++ show path
instance Exception EntrySelectorException
----------------------------------------------------------------------------
-- Entry description
-- | The information about archive entry that can be stored in a zip
-- archive. It does not mirror local file header or central directory file
-- header, but their binary representations can be built given this data
-- structure and the archive contents.
data EntryDescription = EntryDescription
{ -- | Version made by
edVersionMadeBy :: Version,
-- | Version needed to extract
edVersionNeeded :: Version,
-- | Compression method
edCompression :: CompressionMethod,
-- | Last modification date and time
edModTime :: UTCTime,
-- | CRC32 check sum
edCRC32 :: Word32,
-- | Size of compressed entry
edCompressedSize :: Natural,
-- | Size of uncompressed entry
edUncompressedSize :: Natural,
-- | Absolute offset of local file header
edOffset :: Natural,
-- | Entry comment
edComment :: Maybe Text,
-- | All extra fields found
edExtraField :: Map Word16 ByteString,
-- | External file attributes
--
-- @since 1.2.0
edExternalFileAttrs :: Word32
}
deriving (Eq, Typeable)
-- | The supported compression methods.
data CompressionMethod
= -- | Store file uncompressed
Store
| -- | Deflate
Deflate
| -- | Compressed using BZip2 algorithm
BZip2
| -- | Compressed using Zstandard algorithm
--
-- @since 1.6.0
Zstd
deriving (Show, Read, Eq, Ord, Enum, Bounded, Data, Typeable)
----------------------------------------------------------------------------
-- Archive description
-- | The information about the archive as a whole.
data ArchiveDescription = ArchiveDescription
{ -- | The comment of the entire archive
adComment :: Maybe Text,
-- | Absolute offset of the start of central directory
adCDOffset :: Natural,
-- | The size of central directory record
adCDSize :: Natural
}
deriving (Show, Read, Eq, Ord, Typeable, Data)
----------------------------------------------------------------------------
-- Exceptions
-- | The bad things that can happen when you use the library.
data ZipException
= -- | Thrown when you try to get contents of non-existing entry
EntryDoesNotExist FilePath EntrySelector
| -- | Thrown when attempting to decompress an entry compressed with an
-- unsupported compression method or the library is compiled without
-- support for it.
--
-- @since 2.0.0
UnsupportedCompressionMethod CompressionMethod
| -- | Thrown when archive structure cannot be parsed.
ParsingFailed FilePath String
deriving (Eq, Ord, Typeable)
instance Show ZipException where
show (EntryDoesNotExist file s) =
"No such entry found: " ++ show s ++ " in " ++ show file
show (ParsingFailed file msg) =
"Parsing of archive structure failed: \n" ++ msg ++ "\nin " ++ show file
show (UnsupportedCompressionMethod method) =
"Encountered a zipfile entry with "
++ show method
++ " compression, but "
++ "zip library does not support it or has been built without support for it."
instance Exception ZipException
|