From f4f1adb8f302ce986c2ed9d65934da51f41dd606 Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Fri, 13 Jul 2018 23:00:07 -0300 Subject: fs: make unpack_archive platform-agnostic using specific fs functions Use luarocks.tools.tar for handling tar files, and add platform-specific functions fs.zip, fs.unzip, fs.bunzip2, fs.gunzip, giving them native implementations using Lua modules or alternative implementations using third-party tools. --- binary/all_in_one | 1 - spec/fixtures/abc.bz2 | Bin 0 -> 66 bytes spec/fs_spec.lua | 16 ++ src/luarocks/fs/lua.lua | 148 ++++++++++------ src/luarocks/fs/unix/tools.lua | 60 +++---- src/luarocks/fs/win32/tools.lua | 102 +++++------ src/luarocks/tools/zip.lua | 368 ++++++++++++++++++++++++++++++++++------ 7 files changed, 504 insertions(+), 191 deletions(-) create mode 100644 spec/fixtures/abc.bz2 diff --git a/binary/all_in_one b/binary/all_in_one index 882289f5..dcef0fbf 100755 --- a/binary/all_in_one +++ b/binary/all_in_one @@ -463,7 +463,6 @@ local function main() local dependencies = { md5 = "md5", - luazip = if_platform("unix", "luazip"), luasec = "./binary/luasec-0.7alpha-2.rockspec", luaposix = if_platform("unix", "./binary/luaposix-34.0.4-1.rockspec"), luasocket = "luasocket", diff --git a/spec/fixtures/abc.bz2 b/spec/fixtures/abc.bz2 new file mode 100644 index 00000000..ee786715 Binary files /dev/null and b/spec/fixtures/abc.bz2 differ diff --git a/spec/fs_spec.lua b/spec/fs_spec.lua index 66453404..5bec6168 100644 --- a/spec/fs_spec.lua +++ b/spec/fs_spec.lua @@ -1293,6 +1293,22 @@ describe("Luarocks fs test #unit", function() assert.falsy(exists_file("nonexistent")) end) end) + + describe("fs.bunzip2", function() + + it("uncompresses a .bz2 file", function() + local input = testing_paths.fixtures_dir .. "/abc.bz2" + local output = os.tmpname() + assert.truthy(fs.bunzip2(input, output)) + local fd = io.open(output, "r") + local content = fd:read("*a") + fd:close() + assert.same(300000, #content) + local abc = ("a"):rep(100000)..("b"):rep(100000)..("c"):rep(100000) + assert.same(abc, content) + end) + + end) describe("fs.unzip", function() local tmpdir diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua index f2fa50b6..375cdee3 100644 --- a/src/luarocks/fs/lua.lua +++ b/src/luarocks/fs/lua.lua @@ -1,6 +1,6 @@ --- Native Lua implementation of filesystem and platform abstractions, --- using LuaFileSystem, LZLib, MD5 and LuaCurl. +-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5. -- module("luarocks.fs.lua") local fs_lua = {} @@ -10,20 +10,21 @@ local cfg = require("luarocks.core.cfg") local dir = require("luarocks.dir") local util = require("luarocks.util") -local socket_ok, zip_ok, unzip_ok, lfs_ok, md5_ok, posix_ok, _ -local http, ftp, lrzip, luazip, lfs, md5, posix +local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _ +local http, ftp, zip, lfs, md5, posix, bz2 if cfg.fs_use_modules then socket_ok, http = pcall(require, "socket.http") _, ftp = pcall(require, "socket.ftp") - zip_ok, lrzip = pcall(require, "luarocks.tools.zip") - unzip_ok, luazip = pcall(require, "zip"); _G.zip = nil + zip_ok, zip = pcall(require, "luarocks.tools.zip") + bz2_ok, bz2 = pcall(require, "luarocks.tools.bzip2") lfs_ok, lfs = pcall(require, "lfs") md5_ok, md5 = pcall(require, "md5") posix_ok, posix = pcall(require, "posix") end local patch = require("luarocks.tools.patch") +local tar = require("luarocks.tools.tar") local dir_stack = {} @@ -589,52 +590,54 @@ end end --------------------------------------------------------------------- --- LuaZip functions +-- lua-bz2 functions +--------------------------------------------------------------------- + +if bz2_ok then + +local function bunzip2_string(data) + local decompressor = bz2.initDecompress() + local output, err = decompressor:update(data) + if not output then + return nil, err + end + decompressor:close() + return output +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function fs_lua.bunzip2(infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%.bz2$", "") + end + + return fs.filter_file(bunzip2_string, infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- luarocks.tools.zip functions --------------------------------------------------------------------- if zip_ok then function fs_lua.zip(zipfile, ...) - return lrzip.zip(zipfile, ...) + return zip.zip(zipfile, ...) end +function fs_lua.unzip(zipfile) + return zip.unzip(zipfile) end -if unzip_ok then ---- Uncompress files from a .zip archive. --- @param filename string: pathname of .zip archive to be extracted. --- @return boolean: true on success, false on failure. -function fs_lua.unzip(filename) - local zipfile, err = luazip.open(filename) - if not zipfile then return nil, err end - local files = zipfile:files() - local file = files() - repeat - if file.filename:sub(#file.filename) == "/" then - local ok, err = fs.make_dir(dir.path(fs.current_dir(), file.filename)) - if not ok then return nil, err end - else - local base = dir.dir_name(file.filename) - if base ~= "" then - base = dir.path(fs.current_dir(), base) - if not fs.is_dir(base) then - local ok, err = fs.make_dir(base) - if not ok then return nil, err end - end - end - local rf, err = zipfile:open(file.filename) - if not rf then zipfile:close(); return nil, err end - local contents = rf:read("*a") - rf:close() - local wf, err = io.open(dir.path(fs.current_dir(), file.filename), "wb") - if not wf then zipfile:close(); return nil, err end - wf:write(contents) - wf:close() - end - file = files() - until not file - zipfile:close() - return true +function fs_lua.gunzip(infile, outfile) + return zip.gunzip(infile, outfile) end end @@ -878,6 +881,16 @@ local octal_to_rwx = { ["7"] = "rwx", } +function fs_lua._unix_rwx_to_number(rwx) + local num = 0 + for i = 1, 9 do + if rwx:sub(10 - i, 10 - i) == "-" then + num = num + 2^i + end + end + return num +end + do local umask_cache function fs_lua._unix_umask() @@ -886,13 +899,8 @@ do end -- LuaPosix (as of 34.0.4) only returns the umask as rwx local rwx = posix.umask() - local oct = 0 - for i = 1, 9 do - if rwx:sub(10 - i, 10 - i) == "-" then - oct = oct + 2^i - end - end - umask_cache = ("%03o"):format(oct) + local num = fs_lua._unix_rwx_to_number(rwx) + umask_cache = ("%03o"):format(num) return umask_cache end end @@ -1070,4 +1078,46 @@ function fs_lua.is_lua(filename) return (result == true) end +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function fs_lua.unpack_archive(archive) + assert(type(archive) == "string") + + local ok, err + archive = fs.absolute_name(archive) + if archive:match("%.tar%.gz$") then + local tar_filename = archive:gsub("%.gz$", "") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tgz$") then + local tar_filename = archive:gsub("%.tgz$", ".tar") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tar%.bz2$") then + local tar_filename = archive:gsub("%.bz2$", "") + ok, err = fs.bunzip2(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.zip$") then + ok, err = fs.unzip(archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + return false, "Couldn't extract archive "..archive..": unrecognized filename extension" + end + if not ok then + return false, "Failed extracting "..archive..": "..err + end + return true +end + return fs_lua diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua index de63eb81..d6277119 100644 --- a/src/luarocks/fs/unix/tools.lua +++ b/src/luarocks/fs/unix/tools.lua @@ -136,6 +136,33 @@ function tools.unzip(zipfile) return fs.execute_quiet(vars.UNZIP, zipfile) end +local function uncompress(default_ext, program, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%."..default_ext.."$", "") + end + return fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return uncompress("gz", "gunzip", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return uncompress("bz2", "bunzip2", infile, outfile) +end + --- Test is file/directory exists -- @param file string: filename to test -- @return boolean: true if file exists, false otherwise. @@ -198,39 +225,6 @@ function tools.set_permissions(filename, mode, scope) return fs.execute(vars.CHMOD, perms, filename) end ---- Unpack an archive. --- Extract the contents of an archive, detecting its format by --- filename extension. --- @param archive string: Filename of archive. --- @return boolean or (boolean, string): true on success, false and an error message on failure. -function tools.unpack_archive(archive) - assert(type(archive) == "string") - - local pipe_to_tar = " | "..vars.TAR.." -xf -" - - if not cfg.verbose then - pipe_to_tar = " 2> /dev/null"..fs.quiet(pipe_to_tar) - end - - local ok - if archive:match("%.tar%.gz$") or archive:match("%.tgz$") then - ok = fs.execute_string(vars.GUNZIP.." -c "..fs.Q(archive)..pipe_to_tar) - elseif archive:match("%.tar%.bz2$") then - ok = fs.execute_string(vars.BUNZIP2.." -c "..fs.Q(archive)..pipe_to_tar) - elseif archive:match("%.zip$") then - ok = fs.execute_quiet(vars.UNZIP, archive) - elseif archive:match("%.lua$") or archive:match("%.c$") then - -- Ignore .lua and .c files; they don't need to be extracted. - return true - else - return false, "Couldn't extract archive "..archive..": unrecognized filename extension" - end - if not ok then - return false, "Failed extracting "..archive - end - return true -end - function tools.attributes(filename, attrtype) local flag = ((attrtype == "permissions") and vars.STATPERMFLAG) or ((attrtype == "owner") and vars.STATOWNERFLAG) diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua index 0a27cbe3..633aae5e 100644 --- a/src/luarocks/fs/win32/tools.lua +++ b/src/luarocks/fs/win32/tools.lua @@ -148,6 +148,51 @@ function tools.unzip(zipfile) return fs.execute_quiet(fs.Q(vars.SEVENZ).." -aoa x", zipfile) end +local function sevenz(default_ext, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + + local dropext = infile:gsub("%."..default_ext.."$", "") + local outdir = dir.dir_name(dropext) + + infile = fs.absolute_name(infile) + + local cmdline = fs.Q(vars.SEVENZ).." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile) + local ok, err = fs.execute_quiet(cmdline) + if not ok then + return nil, err + end + + if outfile then + outfile = fs.absolute_name(outfile) + dropext = fs.absolute_name(dropext) + ok, err = os.rename(dropext, outfile) + if not ok then + return nil, err + end + end + + return true +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return sevenz("gz", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return sevenz("bz2", infile, outfile) +end + --- Test is pathname is a directory. -- @param file string: pathname to test -- @return boolean: true if it is a directory, false otherwise. @@ -241,63 +286,6 @@ function tools.set_permissions(filename, mode, scope) return true end - ---- Strip the last extension of a filename. --- Example: "foo.tar.gz" becomes "foo.tar". --- If filename has no dots, returns it unchanged. --- @param filename string: The file name to strip. --- @return string: The stripped name. -local function strip_extension(filename) - assert(type(filename) == "string") - return (filename:gsub("%.[^.]+$", "")) or filename -end - ---- Uncompress gzip file. --- @param archive string: Filename of archive. --- @return boolean : success status -local function gunzip(archive) - return fs.execute_quiet(fs.Q(vars.SEVENZ).." -aoa x", archive) -end - ---- Unpack an archive. --- Extract the contents of an archive, detecting its format by --- filename extension. --- @param archive string: Filename of archive. --- @return boolean or (boolean, string): true on success, false and an error message on failure. -function tools.unpack_archive(archive) - assert(type(archive) == "string") - - local ok - local sevenzx = fs.Q(vars.SEVENZ).." -aoa x" - if archive:match("%.tar%.gz$") then - ok = gunzip(archive) - if ok then - ok = fs.execute_quiet(sevenzx, strip_extension(archive)) - end - elseif archive:match("%.tgz$") then - ok = gunzip(archive) - if ok then - ok = fs.execute_quiet(sevenzx, strip_extension(archive)..".tar") - end - elseif archive:match("%.tar%.bz2$") then - ok = fs.execute_quiet(sevenzx, archive) - if ok then - ok = fs.execute_quiet(sevenzx, strip_extension(archive)) - end - elseif archive:match("%.zip$") then - ok = fs.execute_quiet(sevenzx, archive) - elseif archive:match("%.lua$") or archive:match("%.c$") then - -- Ignore .lua and .c files; they don't need to be extracted. - return true - else - return false, "Couldn't extract archive "..archive..": unrecognized filename extension" - end - if not ok then - return false, "Failed extracting "..archive - end - return true -end - --- Test for existance of a file. -- @param file string: filename to test -- @return boolean: true if file exists, false otherwise. diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua index e6d9e36a..5974c7bf 100644 --- a/src/luarocks/tools/zip.lua +++ b/src/luarocks/tools/zip.lua @@ -1,26 +1,58 @@ ---- A Lua implementation of .zip file archiving (used for creating .rock files), +--- A Lua implementation of .zip and .gz file compression and decompression, -- using only lzlib or lua-lzib. local zip = {} local zlib = require("zlib") local fs = require("luarocks.fs") +local fun = require("luarocks.fun") local dir = require("luarocks.dir") +local stat_ok, stat = pcall(require, "posix.sys.stat") + +local function shr(n, m) + return math.floor(n / 2^m) +end + +local function shl(n, m) + return n * 2^m +end +local function lowbits(n, m) + return n % 2^m +end + +local function mode_to_windowbits(mode) + if mode == "gzip" then + return 31 + elseif mode == "zlib" then + return 0 + elseif mode == "raw" then + return -15 + end +end + -- zlib module can be provided by both lzlib and lua-lzib packages. -- Create a compatibility layer. -local zlib_compress, zlib_crc32 +local zlib_compress, zlib_uncompress, zlib_crc32 if zlib._VERSION:match "^lua%-zlib" then - function zlib_compress(data) - return (zlib.deflate()(data, "finish")) + function zlib_compress(data, mode) + return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish")) + end + + function zlib_uncompress(data, mode) + return (zlib.inflate(mode_to_windowbits(mode))(data)) end function zlib_crc32(data) return zlib.crc32()(data) end elseif zlib._VERSION:match "^lzlib" then - function zlib_compress(data) - return zlib.compress(data) + function zlib_compress(data, mode) + return zlib.compress(data, -1, nil, mode_to_windowbits(mode)) + end + + function zlib_uncompress(data, mode) + return zlib.decompress(data, mode_to_windowbits(mode)) end function zlib_crc32(data) @@ -30,7 +62,7 @@ else error("unknown zlib library", 0) end -local function number_to_bytestring(number, nbytes) +local function number_to_lestring(number, nbytes) local out = {} for _ = 1, nbytes do local byte = number % 256 @@ -40,6 +72,20 @@ local function number_to_bytestring(number, nbytes) return table.concat(out) end +local function lestring_to_number(str) + local n = 0 + local bytes = { string.byte(str, 1, #str) } + for b = 1, #str do + n = n + shl(bytes[b], (b-1)*8) + end + return math.floor(n) +end + +local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4) +local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4) +local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4) +local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4) + --- Begin a new file to be stored inside the zipfile. -- @param self handle of the zipfile being written. -- @param filename filenome of the file to be added to the zipfile. @@ -56,7 +102,7 @@ local function zipwriter_open_new_file_in_zip(self, filename) lfh.file_name_length = #filename lfh.extra_field_length = 0 lfh.file_name = filename:gsub("\\", "/") - lfh.external_attr = 0 -- TODO properly store permissions + lfh.external_attr = shl(493, 16) -- TODO proper permissions self.in_open_file = true return true end @@ -70,7 +116,7 @@ local function zipwriter_write_file_in_zip(self, data) return nil end local lfh = self.local_file_header - local compressed = zlib_compress(data):sub(3, -5) + local compressed = zlib_compress(data, "raw") lfh.crc32 = zlib_crc32(data) lfh.compressed_size = #compressed lfh.uncompressed_size = #data @@ -91,26 +137,27 @@ local function zipwriter_close_file_in_zip(self) -- Local file header local lfh = self.local_file_header lfh.offset = zh:seek() - zh:write(number_to_bytestring(0x04034b50, 4)) -- signature - zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 - zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag - zh:write(number_to_bytestring(8, 2)) -- compression method: deflate - zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) - zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) - zh:write(number_to_bytestring(lfh.crc32, 4)) - zh:write(number_to_bytestring(lfh.compressed_size, 4)) - zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) - zh:write(number_to_bytestring(lfh.file_name_length, 2)) - zh:write(number_to_bytestring(lfh.extra_field_length, 2)) + zh:write(LOCAL_FILE_HEADER_SIGNATURE) + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(4, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) zh:write(lfh.file_name) -- File data zh:write(self.data) -- Data descriptor - zh:write(number_to_bytestring(lfh.crc32, 4)) - zh:write(number_to_bytestring(lfh.compressed_size, 4)) - zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) + zh:write(DATA_DESCRIPTOR_SIGNATURE) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) table.insert(self.files, lfh) self.in_open_file = false @@ -167,36 +214,36 @@ local function zipwriter_close(self) local size_of_central_directory = 0 -- Central directory structure for _, lfh in ipairs(self.files) do - zh:write(number_to_bytestring(0x02014b50, 4)) -- signature - zh:write(number_to_bytestring(3, 2)) -- version made by: UNIX - zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 - zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag - zh:write(number_to_bytestring(8, 2)) -- compression method: deflate - zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) - zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) - zh:write(number_to_bytestring(lfh.crc32, 4)) - zh:write(number_to_bytestring(lfh.compressed_size, 4)) - zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) - zh:write(number_to_bytestring(lfh.file_name_length, 2)) - zh:write(number_to_bytestring(lfh.extra_field_length, 2)) - zh:write(number_to_bytestring(0, 2)) -- file comment length - zh:write(number_to_bytestring(0, 2)) -- disk number start - zh:write(number_to_bytestring(0, 2)) -- internal file attributes - zh:write(number_to_bytestring(lfh.external_attr, 4)) -- external file attributes - zh:write(number_to_bytestring(lfh.offset, 4)) -- relative offset of local header + zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature + zh:write(number_to_lestring(3, 2)) -- version made by: UNIX + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(0, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) + zh:write(number_to_lestring(0, 2)) -- file comment length + zh:write(number_to_lestring(0, 2)) -- disk number start + zh:write(number_to_lestring(0, 2)) -- internal file attributes + zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes + zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header zh:write(lfh.file_name) size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length end -- End of central directory record - zh:write(number_to_bytestring(0x06054b50, 4)) -- signature - zh:write(number_to_bytestring(0, 2)) -- number of this disk - zh:write(number_to_bytestring(0, 2)) -- number of disk with start of central directory - zh:write(number_to_bytestring(#self.files, 2)) -- total number of entries in the central dir on this disk - zh:write(number_to_bytestring(#self.files, 2)) -- total number of entries in the central dir - zh:write(number_to_bytestring(size_of_central_directory, 4)) - zh:write(number_to_bytestring(central_directory_offset, 4)) - zh:write(number_to_bytestring(0, 2)) -- zip file comment length + zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature + zh:write(number_to_lestring(0, 2)) -- number of this disk + zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir + zh:write(number_to_lestring(size_of_central_directory, 4)) + zh:write(number_to_lestring(central_directory_offset, 4)) + zh:write(number_to_lestring(0, 2)) -- zip file comment length zh:close() return true @@ -253,12 +300,231 @@ function zip.zip(zipfile, ...) end end - ok = zw:close() + zw:close() + return ok, err +end + + +local function ziptime_to_luatime(ztime, zdate) + return { + year = shr(zdate, 9) + 1980, + month = shr(lowbits(zdate, 9), 5), + day = lowbits(zdate, 5), + hour = shr(ztime, 11), + min = shr(lowbits(ztime, 11), 5), + sec = lowbits(ztime, 5) * 2, + } +end + +local function read_file_in_zip(zh, cdr) + local sig = zh:read(4) + if sig ~= LOCAL_FILE_HEADER_SIGNATURE then + return nil, "failed reading Local File Header signature" + end + + local lfh = {} + lfh.version_needed = lestring_to_number(zh:read(2)) + lfh.bitflag = lestring_to_number(zh:read(2)) + lfh.compression_method = lestring_to_number(zh:read(2)) + lfh.last_mod_file_time = lestring_to_number(zh:read(2)) + lfh.last_mod_file_date = lestring_to_number(zh:read(2)) + lfh.crc32 = lestring_to_number(zh:read(4)) + lfh.compressed_size = lestring_to_number(zh:read(4)) + lfh.uncompressed_size = lestring_to_number(zh:read(4)) + lfh.file_name_length = lestring_to_number(zh:read(2)) + lfh.extra_field_length = lestring_to_number(zh:read(2)) + lfh.file_name = zh:read(lfh.file_name_length) + lfh.extra_field = zh:read(lfh.extra_field_length) + + local data = zh:read(cdr.compressed_size) + + local uncompressed + if cdr.compression_method == 8 then + uncompressed = zlib_uncompress(data, "raw") + elseif cdr.compression_method == 0 then + uncompressed = data + else + return nil, "unknown compression method " .. cdr.compression_method + end + + if #uncompressed ~= cdr.uncompressed_size then + return nil, "uncompressed size doesn't match" + end + if cdr.crc32 ~= zlib_crc32(uncompressed) then + return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed + end + + return uncompressed +end + +local function process_end_of_central_dir(zh) + local at, err = zh:seek("end", -22) + if not at then + return nil, err + end + + while true do + local sig = zh:read(4) + if sig == END_OF_CENTRAL_DIR_SIGNATURE then + break + end + at = at - 1 + local at1, err = zh:seek("set", at) + if at1 ~= at then + return nil, "Could not find End of Central Directory signature" + end + end + + -- number of this disk (2 bytes) + -- number of the disk with the start of the central directory (2 bytes) + -- total number of entries in the central directory on this disk (2 bytes) + -- total number of entries in the central directory (2 bytes) + zh:seek("cur", 6) + + local central_directory_entries = lestring_to_number(zh:read(2)) + + -- central directory size (4 bytes) + zh:seek("cur", 4) + + local central_directory_offset = lestring_to_number(zh:read(4)) + + return central_directory_entries, central_directory_offset +end + +local function process_central_dir(zh, cd_entries) + + local files = {} + + for i = 1, cd_entries do + local sig = zh:read(4) + if sig ~= CENTRAL_DIRECTORY_SIGNATURE then + return nil, "failed reading Central Directory signature" + end + + local cdr = {} + files[i] = cdr + + cdr.version_made_by = lestring_to_number(zh:read(2)) + cdr.version_needed = lestring_to_number(zh:read(2)) + cdr.bitflag = lestring_to_number(zh:read(2)) + cdr.compression_method = lestring_to_number(zh:read(2)) + cdr.last_mod_file_time = lestring_to_number(zh:read(2)) + cdr.last_mod_file_date = lestring_to_number(zh:read(2)) + cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date) + cdr.crc32 = lestring_to_number(zh:read(4)) + cdr.compressed_size = lestring_to_number(zh:read(4)) + cdr.uncompressed_size = lestring_to_number(zh:read(4)) + cdr.file_name_length = lestring_to_number(zh:read(2)) + cdr.extra_field_length = lestring_to_number(zh:read(2)) + cdr.file_comment_length = lestring_to_number(zh:read(2)) + cdr.disk_number_start = lestring_to_number(zh:read(2)) + cdr.internal_attr = lestring_to_number(zh:read(2)) + cdr.external_attr = lestring_to_number(zh:read(4)) + cdr.offset = lestring_to_number(zh:read(4)) + cdr.file_name = zh:read(cdr.file_name_length) + cdr.extra_field = zh:read(cdr.extra_field_length) + cdr.file_comment = zh:read(cdr.file_comment_length) + end + return files +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +function zip.unzip(zipfile) + zipfile = fs.absolute_name(zipfile) + local zh, err = io.open(zipfile, "rb") + if not zh then + return nil, err + end + + local cd_entries, cd_offset = process_end_of_central_dir(zh) + if not cd_entries then + return nil, cd_offset + end + + local ok, err = zh:seek("set", cd_offset) if not ok then - return false, "error closing "..zipfile + return nil, err end - return ok, err + + local files, err = process_central_dir(zh, cd_entries) + if not files then + return nil, err + end + + for _, cdr in ipairs(files) do + local file = cdr.file_name + if file:sub(#file) == "/" then + local ok, err = fs.make_dir(dir.path(fs.current_dir(), file)) + if not ok then + return nil, err + end + else + local base = dir.dir_name(file) + if base ~= "" then + base = dir.path(fs.current_dir(), base) + if not fs.is_dir(base) then + local ok, err = fs.make_dir(base) + if not ok then + return nil, err + end + end + end + + local ok, err = zh:seek("set", cdr.offset) + if not ok then + return nil, err + end + + local contents, err = read_file_in_zip(zh, cdr) + if not contents then + return nil, err + end + local pathname = dir.path(fs.current_dir(), file) + local wf, err = io.open(pathname, "wb") + if not wf then + zh:close() + return nil, err + end + wf:write(contents) + wf:close() + + if cdr.external_attr > 0 then + fs.set_permissions(pathname, "exec", "all") + else + fs.set_permissions(pathname, "read", "all") + end + fs.set_time(pathname, cdr.last_mod_luatime) + end + end + zh:close() + return true end +function zip.gzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename .. ".gz" + end + + local fn = fun.partial(fun.flip(zlib_compress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end + +function zip.gunzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename:gsub("%.gz$", "") + end + + local fn = fun.partial(fun.flip(zlib_uncompress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end return zip -- cgit v1.2.3-55-g6feb