hslua-module-zip (empty) → 1.0.0
raw patch · 7 files changed
+820/−0 lines, 7 filesdep +basedep +bytestringdep +filepath
Dependencies added: base, bytestring, filepath, hslua-core, hslua-list, hslua-marshalling, hslua-module-system, hslua-module-zip, hslua-packaging, tasty, tasty-hunit, tasty-lua, text, time, zip-archive
Files
- CHANGELOG.md +11/−0
- LICENSE +21/−0
- README.md +159/−0
- hslua-module-zip.cabal +80/−0
- src/HsLua/Module/Zip.hs +297/−0
- test/test-hslua-module-zip.hs +61/−0
- test/test-zip.lua +191/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog++`hslua-module-zips` uses [PVP Versioning][].++## hslua-module-zip-1.0.0++Release pending.++- Initially created.++[PVP Versioning]: https://pvp.haskell.org
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright © 2020-2022 Albert Krewinkel++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,159 @@+# hslua-module-zip++[![GitHub CI][CI badge]](https://github.com/hslua/hslua/actions)+[![Hackage][Hackage badge]](https://hackage.haskell.org/package/hslua-module-zip)+[![Stackage Lts][Stackage Lts badge]](http://stackage.org/lts/package/hslua-module-zip)+[![Stackage Nightly][Stackage Nightly badge]](http://stackage.org/nightly/package/hslua-module-zip)+[![MIT license][License badge]](LICENSE)++[CI badge]: https://img.shields.io/github/workflow/status/hslua/hslua/CI.svg?logo=github+[Hackage badge]: https://img.shields.io/hackage/v/hslua-module-zip.svg?logo=haskell+[Stackage Lts badge]: http://stackage.org/package/hslua-module-zip/badge/lts+[Stackage Nightly badge]: http://stackage.org/package/hslua-module-zip/badge/nightly+[License badge]: https://img.shields.io/badge/license-MIT-blue.svg++Lua module to work with file zips.++## zip++Functions to create, modify, and extract files from zip archives.++The module can be called as a function, in which case it behaves+like the `zip` function described below.++Zip options are optional; when defined, they must be a table with+any of the following keys:++ - `recursive`: recurse directories when set to `true`;+ - `verbose`: print info messages to stdout;+ - `destination`: the value specifies the directory in which to extract;+ - `location`: value is used as path name, defining where files+ are placed.+ - `preserve_symlinks`: Boolean value, controlling whether+ symbolic links are preserved as such. This option is ignored+ on Windows.++## Functions++### Archive++`Archive (bytestring_or_entries)`++Reads an *Archive* structure from a raw zip archive or a list of+Entry items; throws an error if the given string cannot be decoded+into an archive.++*Since: 1.0.0*++Parameters:++bytestring_or_entries+: (string|{ZipEntry,...})++Returns:++ - (ZipArchive)++### Entry++`Entry (path, contents[, modtime])`++Generates a zip Entry from a filepath, the file's uncompressed+content, and the file's modification time.++*Since: 1.0.0*++Parameters:++path+: file path in archive (string)++contents+: uncompressed contents (string)++modtime+: modification time (integer)++### read_entry++`read_entry (filepath, opts)`++Generates a ZipEntry from a file or directory.++*Since: 1.0.0*++Parameters:++filepath+: (string)++opts+: zip options (table)++Returns:++ - a new zip archive entry (ZipEntry)++### zip++`zip (filepaths[, options])`++Package and compress the given files into a new Archive.++*Since: 1.0.0*++Parameters:++filepaths+: list of files from which the archive is created. ({string,...})++options+: zip options (table)++Returns:++ - a new archive (ZipArchive)++## Types++### Archive++A zip archive with file entries.++#### Fields++`entries`+: files in this zip archive ({Entry,...})++#### Methods++`extract([opts])`+: Extract all files from this archive, creating directories as+ needed. Note that the last-modified time is set correctly only+ in POSIX, not in Windows. This function fails if encrypted+ entries are present.++ Use `archive:extract{destination = 'dir'}` to extract to+ subdirectory `dir`.++`bytestring()`+: Returns the raw binary string representation of the archive.++### Entry++File or directory entry in a zip archive.++#### Fields:++`path`+: relative path, using `/` as separator++`modtime`+: modification time (seconds since unix epoch)++#### Methods:++`contents([password])`+: Get the uncompressed contents of a zip entry. If `password` is+ given, then that password is used to decrypt the contents. An+ error is throws if decrypting fails.
+ hslua-module-zip.cabal view
@@ -0,0 +1,80 @@+cabal-version: 2.2+name: hslua-module-zip+version: 1.0.0+synopsis: Lua module to work with file zips.+description: Module with function for creating, modifying, and+ extracting files from zip archives.+homepage: https://hslua.org/+bug-reports: https://github.com/hslua/hslua/issues+license: MIT+license-file: LICENSE+author: Albert Krewinkel+maintainer: Albert Krewinkel <albert@zeitkraut.de>+copyright: © 2020-2022 Albert Krewinkel+category: Foreign+build-type: Simple+extra-doc-files: README.md+ CHANGELOG.md+extra-source-files: test/test-zip.lua+tested-with: GHC == 8.0.2+ , GHC == 8.2.2+ , GHC == 8.4.4+ , GHC == 8.6.5+ , GHC == 8.8.4+ , GHC == 8.10.7+ , GHC == 9.0.2+ , GHC == 9.2.3++source-repository head+ type: git+ location: https://github.com/hslua/hslua+ subdir: hslua-module-zip++common common-options+ build-depends: base >= 4.9.1 && < 5+ , bytestring+ , filepath >= 1.4 && < 1.5+ , hslua-core >= 2.1 && < 2.3+ , hslua-list >= 1.1 && < 1.2+ , hslua-marshalling >= 2.1 && < 2.3+ , hslua-packaging >= 2.1 && < 2.3+ , text >= 1.2 && < 2.1+ , time >= 1.5 && < 1.14+ , zip-archive >= 0.4 && < 0.5++ ghc-options: -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ if impl(ghc >= 8.0)+ ghc-options: -Wredundant-constraints+ if impl(ghc >= 8.2)+ ghc-options: -fhide-source-paths+ if impl(ghc >= 8.4)+ ghc-options: -Wmissing-export-lists+ -Wpartial-fields+ if impl(ghc >= 8.8)+ ghc-options: -Wmissing-deriving-strategies++ default-language: Haskell2010++library+ import: common-options+ hs-source-dirs: src+ exposed-modules: HsLua.Module.Zip++test-suite hslua-module-zip-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: test-hslua-module-zip.hs+ build-depends: hslua-module-zip+ , hslua-module-system+ , tasty+ , tasty-hunit+ , tasty-lua >= 1.0 && < 1.1+ , text+ ghc-options: -threaded+ -rtsopts+ -with-rtsopts=-N
+ src/HsLua/Module/Zip.hs view
@@ -0,0 +1,297 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-|+Module : HsLua.Module.Zip+Copyright : © 2022 Albert Krewinkel+License : MIT+Maintainer : Albert Krewinkel <albert+hslua@zeitkraut.de>++Lua module to work with file zips.+-}+module HsLua.Module.Zip (+ -- * Module+ documentedModule++ -- * Zip archives+ , typeArchive+ , mkArchive+ , read_entry+ , zip+ -- ** archive methods+ , extract+ , bytestring+ -- * Zip entry+ , typeEntry+ , peekEntryFuzzy+ -- ** entry methods+ , contents+ -- * Zip Options+ , peekZipOptions+ )+where++import Prelude hiding (zip)+import Control.Applicative (optional)+import Control.Monad ((<$!>))+import Codec.Archive.Zip (Archive, Entry, ZipOption (..), emptyArchive)+import Data.Maybe (catMaybes, fromMaybe)+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup (Semigroup(..)) -- includes (<>)+#endif+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Version (Version, makeVersion)+import HsLua.Core+ ( LuaError, NumArgs (..), NumResults (..), Type(..), call, failLua+ , fromStackIndex, getfield, gettop, replace, liftIO, ltype+ , nth, nthBottom, setmetatable )+import HsLua.List (newListMetatable)+import HsLua.Marshalling+ ( Peeker, Pusher, choice, failPeek, liftLua, peekBool+ , peekFieldRaw, peekIntegral, peekLazyByteString, peekList, peekString+ , pushLazyByteString, pushList, pushIntegral, pushString+ , retrieving, typeMismatchMessage )+import HsLua.Packaging++import qualified Codec.Archive.Zip as Zip+import qualified Data.Text as T++#if MIN_VERSION_base(4,11,0)+import Data.Functor ((<&>))+#else+(<&>) :: Functor f => f a -> (a -> b) -> f b+(<&>) = flip fmap+#endif++-- | The @zip@ module specification.+documentedModule :: forall e. LuaError e => Module e+documentedModule = Module+ { moduleName = "zip"+ , moduleDescription = T.unwords+ [ "Function for creating, modifying, and extracting files from zip"+ , "archives."+ ]+ , moduleFields = fields+ , moduleFunctions = functions+ , moduleOperations =+ [ operation Call $ lambda+ ### (do+ -- call function `zip`+ _ <- getfield (nthBottom 1) (functionName @e zip)+ replace (nthBottom 1)+ nargs <- NumArgs . subtract 1 . fromStackIndex <$> gettop+ call nargs 1+ pure (NumResults 1))+ =?> "new Archive"+ ]+ }++-- | First published version of this library.+initialVersion :: Version+initialVersion = makeVersion [1,0,0]++--+-- Fields+--++-- | Exported fields.+fields :: [Field e]+fields = []+++--+-- Functions+--++-- | Exported functions+functions :: LuaError e => [DocumentedFunction e]+functions =+ [ mkArchive+ , mkEntry+ , read_entry+ , zip+ ]++-- | Creates a new 'Archive' from a list of files.+zip :: LuaError e => DocumentedFunction e+zip = defun "zip"+ ### (\filepaths mopts ->+ let opts = fromMaybe [] mopts+ in liftIO $! Zip.addFilesToArchive opts emptyArchive filepaths)+ <#> parameter (peekList peekString) "{string,...}"+ "filepaths" "list of files from which the archive is created."+ <#> opt (parameter peekZipOptions "table" "opts" "zip options")+ =#> udresult typeArchive "a new archive"+ #? T.unlines+ [ "Package and compress the given files into a new Archive." ]+ `since` initialVersion++-- | Creates a new 'ZipEntry' from a file; wraps 'Zip.readEntry'.+read_entry :: LuaError e => DocumentedFunction e+read_entry = defun "read_entry"+ ### (\filepath mopts -> liftIO $! Zip.readEntry (fromMaybe [] mopts) filepath)+ <#> parameter peekString "string" "filepath" ""+ <#> opt (parameter peekZipOptions "table" "opts" "zip options")+ =#> udresult typeEntry "a new zip archive entry"+ #? T.unlines+ [ "Generates a ZipEntry from a file or directory."+ ]+ `since` initialVersion++--+-- * Options+--+peekZipOptions :: LuaError e => Peeker e [ZipOption]+peekZipOptions = retrieving "Zip options" . \idx -> catMaybes <$> sequence+ [ optional (peekFieldRaw peekBool "recursive" idx) <&> \case+ Just True -> Just OptRecursive+ _ -> Nothing+ , optional (peekFieldRaw peekBool "verbose" idx) <&> \case+ Just True -> Just OptVerbose+ _ -> Nothing+ , optional (peekFieldRaw peekString "destination" idx) <&> \case+ Just fp -> Just (OptDestination fp)+ _ -> Nothing+ , optional (peekFieldRaw peekString "location" idx) <&> \case+ Just fp -> Just (OptLocation fp True)+ _ -> Nothing+ , optional (peekFieldRaw peekBool "preserve_symlinks" idx) <&> \case+ Just True -> (Just OptPreserveSymbolicLinks)+ _ -> Nothing+ ]++--+-- * Archive+--++-- | The Lua 'Archive' type+typeArchive :: LuaError e => DocumentedType e Archive+typeArchive = deftype "ZipArchive"+ []+ [ property "entries" "files in this zip archive"+ (pushEntries, Zip.zEntries)+ (peekList peekEntryFuzzy, \ar entries -> ar { Zip.zEntries = entries })+ , method extract+ , method bytestring+ ]++-- | Wrapper for 'Zip.toArchive'; converts a string into an Archive.+mkArchive :: LuaError e => DocumentedFunction e+mkArchive = defun "Archive"+ ### (\case+ Nothing ->+ pure Zip.emptyArchive+ Just (Left bytestring') ->+ either failLua pure $ Zip.toArchiveOrFail bytestring'+ Just (Right entries) ->+ pure $ foldr Zip.addEntryToArchive emptyArchive entries)+ <#> opt (parameter (choice [ fmap Left . peekLazyByteString+ , fmap Right . peekList peekEntryFuzzy ])+ "string|{ZipEntry,...}" "contents"+ "binary archive data or list of entries")+ =#> udresult typeArchive "new Archive"+ #? T.unlines+ [ "Reads an *Archive* structure from a raw zip archive or a list of"+ , "Entry items; throws an error if the given string cannot be decoded"+ , "into an archive."+ ]+ `since` initialVersion++-- | Returns the raw binary string representation of the archive;+-- wraps 'Zip.extractFilesFromArchive'+extract :: LuaError e => DocumentedFunction e+extract = defun "extract"+ ### (\archive mopts ->+ liftIO $! Zip.extractFilesFromArchive (fromMaybe [] mopts) archive)+ <#> udparam typeArchive "self" ""+ <#> opt (parameter peekZipOptions "table" "opts" "zip options")+ =#> []+ #? T.unlines+ [ "Extract all files from this archive, creating directories as needed."+ , "Note that the last-modified time is set correctly only in POSIX, not"+ , "in Windows. This function fails if encrypted entries are present."+ ]++-- | Returns the raw binary string representation of the archive.+bytestring :: LuaError e => DocumentedFunction e+bytestring = defun "bytestring"+ ### liftPure Zip.fromArchive+ <#> udparam typeArchive "self" ""+ =#> functionResult pushLazyByteString "string" "bytes of the archive"+ #? "Returns the raw binary string representation of the archive."++--+-- * Entry+--++-- | The Lua type for 'Entry' objects.+typeEntry :: LuaError e => DocumentedType e Entry+typeEntry = deftype "ZipEntry"+ []+ [ property "path" "relative path, using `/` as separator"+ (pushString, Zip.eRelativePath)+ (peekString, \entry path -> entry { Zip.eRelativePath = path })+ , property "modtime" "modification time (seconds since unix epoch)"+ (pushIntegral, Zip.eLastModified)+ (peekIntegral, \entry modtime -> entry { Zip.eLastModified = modtime})+ , method contents+ ]++-- | Creates a new 'ZipEntry' from a file; wraps 'Zip.readEntry'.+mkEntry :: LuaError e => DocumentedFunction e+mkEntry = defun "Entry"+ ### (\filepath contents' mmodtime -> do+ modtime <- maybe (floor <$> liftIO getPOSIXTime) pure mmodtime+ pure $ Zip.toEntry filepath modtime contents')+ <#> parameter peekString "string" "path" "file path in archive"+ <#> parameter peekLazyByteString "string" "contents" "uncompressed contents"+ <#> opt (parameter peekIntegral "integer" "modtime" "modification time")+ =#> udresult typeEntry "a new zip archive entry"+ #? T.unlines+ [ "Generates a ZipEntry from a filepath, uncompressed content, and"+ , "the file's modification time."+ ]+ `since` initialVersion++-- | Returns the uncompressed contents of a zip entry.+contents :: LuaError e => DocumentedFunction e+contents = defun "contents"+ ### (\entry mpasswd -> case mpasswd of+ Nothing -> return $! Zip.fromEntry entry+ Just passwd -> case Zip.fromEncryptedEntry passwd entry of+ Just contents' -> return $! contents'+ Nothing -> failLua "Could not decrypt entry.")+ <#> udparam typeEntry "self" ""+ <#> opt (parameter peekString "string" "password" "password for entry")+ =#> functionResult pushLazyByteString "string" "binary contents"+ #? T.unlines+ [ "Get the uncompressed contents of a zip entry. If `password` is given,"+ , "then that password is used to decrypt the contents. An error is throws"+ , "if decrypting fails."+ ]++peekEntryFuzzy :: LuaError e => Peeker e Entry+peekEntryFuzzy = retrieving "ZipEntry" . \idx ->+ liftLua (ltype idx) >>= \case+ TypeUserdata -> peekUD typeEntry idx+ TypeTable -> peekEntryFromTable idx+ _ -> failPeek =<<+ typeMismatchMessage "ZipEntry userdata or table" idx++peekEntryFromTable :: LuaError e => Peeker e Entry+peekEntryFromTable idx = Zip.toEntry+ <$!> peekFieldRaw peekString "path" idx+ <*> (peekFieldRaw (optional . peekIntegral) "modtime" idx >>= \case+ Nothing -> pure 0+ Just t -> pure t)+ <*> peekFieldRaw peekLazyByteString "contents" idx++-- | Pushes a list of entries as an Entries object, i.e., a list with+-- additional methods.+pushEntries :: LuaError e => Pusher e [Entry]+pushEntries es = do+ pushList (pushUD typeEntry) es+ newListMetatable "ZipEntry list" (pure ())+ setmetatable (nth 2)
+ test/test-hslua-module-zip.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-|+Module : Main+Copyright : © 2021-2022 Albert Krewinkel+License : MIT+Maintainer : Albert Krewinkel <albert+hslua@zeitkraut.de>+Stability : stable+Portability : Requires language extensions ForeignFunctionInterface,+ OverloadedStrings.++Tests for the `zip` Lua module.+-}+module Main (main) where++import Control.Monad (void)+import HsLua.Core (Lua, top)+import HsLua.Packaging ( preloadModule, preloadModuleWithName, pushModule+ , registerModule)+import HsLua.Module.Zip (documentedModule)+import Test.Tasty (TestTree, defaultMain, testGroup)+import Test.Tasty.HUnit (assertEqual, testCase)+import Test.Tasty.Lua (translateResultsFromFile)++import qualified HsLua.Core as Lua+import qualified HsLua.Module.System as System++main :: IO ()+main = do+ luaTestResults <- Lua.run @Lua.Exception $ do+ Lua.openlibs+ registerModule documentedModule+ registerModule System.documentedModule+ Lua.pop 1+ translateResultsFromFile "test/test-zip.lua"+ defaultMain $ testGroup "hslua-module-zip" [tests, luaTestResults]++-- | HSpec tests for the Lua 'system' module+tests :: TestTree+tests = testGroup "HsLua zip module"+ [ testCase "zip module can be pushed to the stack" $+ Lua.run (void (pushModule documentedModule) :: Lua ())++ , testCase "zip module can be added to the preloader" . Lua.run $ do+ Lua.openlibs+ preloadModule documentedModule+ assertEqual' "function not added to preloader" Lua.TypeFunction =<< do+ void $ Lua.getglobal "package"+ *> Lua.getfield top "preload"+ *> Lua.getfield top "zip"+ Lua.ltype (-1)++ , testCase "zip module can be loaded as hszip" . Lua.run $ do+ Lua.openlibs+ preloadModuleWithName documentedModule "hszip"+ assertEqual' "loading the module fails " Lua.OK =<<+ Lua.dostring "require 'hszip'"+ ]++assertEqual' :: (Show a, Eq a) => String -> a -> a -> Lua ()+assertEqual' msg expected = Lua.liftIO . assertEqual msg expected
+ test/test-zip.lua view
@@ -0,0 +1,191 @@+--+-- Tests for the system module+--+local zip = require 'zip'+local tasty = require 'tasty'+local system = require 'system'++local group = tasty.test_group+local test = tasty.test_case+local assert = tasty.assert++local make_sample_archive = function (opts)+ opts = opts or {}+ local filename = opts.filename or 'greetings.txt'+ local contents = opts.contents or 'Hello Bob!\n'+ return system.with_tmpdir('archive', function (tmpdir)+ return system.with_wd(tmpdir, function ()+ local fh = io.open(filename, 'w')+ fh:write(contents)+ fh:close()+ return zip{filename}+ end)+ end)+end++local empty_archive = '\80\75\5\6\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'+-- Check existence static fields+return {+ group 'Archive' {+ test('empty archive', function ()+ assert.are_equal(type(zip.Archive()), 'userdata')+ end),+ },++ group 'zip' {+ test('archive with file', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local filename = 'greetings.txt'+ local fh = io.open(filename, 'w')+ fh:write('Hi Mom!\n')+ fh:close()+ assert.are_equal(+ type(zip.zip{filename}),+ 'userdata'+ )+ end)+ end)+ end),++ test('recursive', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local dirname = 'greetings'+ local filename = dirname .. '/' .. 'french.txt'+ system.mkdir(dirname)+ local fh = io.open(filename, 'w')+ fh:write('Bonjour!\n')+ fh:close()+ local archive = zip.zip({dirname}, {recursive=true})+ assert.are_equal(+ archive.entries[2].path,+ filename+ )+ end)+ end)+ end),++ test('module metamethod', function()+ local filename = 'greetings.txt'+ local fh = io.open(filename, 'w')+ fh:write('Hi Mom!\n')+ fh:close()+ assert.are_equal(+ zip.zip{filename}:bytestring(),+ zip{filename}:bytestring()+ )+ end)+ },++ group 'extract' {+ test('archive with file', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local filename = 'greetings.txt'+ local archive = make_sample_archive{+ filename = filename,+ contents = 'Hi Mom!\n',+ }+ archive:extract{destination = 'foo'}+ assert.are_equal(system.ls()[1], 'foo')+ assert.are_equal(+ io.open('foo/' .. filename):read 'a',+ 'Hi Mom!\n'+ )+ end)+ end)+ end),+ test('archive with file', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local filename = 'greetings.txt'+ local archive = make_sample_archive{+ filename = filename,+ contents = 'Hi Mom!\n',+ }+ archive:extract()++ assert.are_equal(system.ls()[1], filename)+ assert.are_equal(+ io.open(filename):read 'a',+ 'Hi Mom!\n'+ )+ end)+ end)+ end)+ },++ group 'entries' {+ test('empty archive', function ()+ assert.are_equal(#zip.Archive().entries, 0)+ end),++ test('archive with file', function ()+ local archive = make_sample_archive{filename='greetings.txt'}+ assert.are_equal(+ archive.entries[1].path,+ 'greetings.txt'+ )+ end),++ test('has type "ZipEntry list"', function ()+ local archive = make_sample_archive()+ assert.are_equal(getmetatable(archive.entries).__name, 'ZipEntry list')+ end),++ test('has `insert` method', function ()+ local archive = make_sample_archive()+ assert.are_equal(type(archive.entries.insert), 'function')+ end),+ },++ group 'read_entry' {+ test('has correct file path', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local filename = 'greetings.txt'+ local fh = io.open(filename, 'w')+ fh:write('Hi Mom!\n')+ fh:close()+ local entry = zip.read_entry(filename)+ assert.are_equal(entry.path, filename)+ end)+ end)+ end),++ test('has contents', function ()+ system.with_tmpdir('archive', function (tmpdir)+ system.with_wd(tmpdir, function ()+ local filename = 'greetings.txt'+ local fh = io.open(filename, 'w')+ fh:write('Hallo!\n')+ fh:close()+ local entry = zip.read_entry(filename)+ assert.are_equal(+ entry:contents('foo'),+ 'Hallo!\n'+ )+ end)+ end)+ end)+ },++ group 'bytestring' {+ test('empty archive', function ()+ assert.are_equal(zip.Archive():bytestring(), empty_archive)+ end),+ },++ group 'Archive constructor' {+ test('empty archive', function ()+ assert.are_equal(type(zip.Archive(empty_archive)), 'userdata')+ end),+ test('misformed archive', function ()+ assert.error_matches(+ function () zip.Archive(empty_archive:sub(2)) end,+ ''+ )+ end),+ },+}