summaryrefslogtreecommitdiff
path: root/src/luarocks/tools/zip.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks/tools/zip.lua')
-rw-r--r--src/luarocks/tools/zip.lua531
1 files changed, 531 insertions, 0 deletions
diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua
new file mode 100644
index 0000000..82d582f
--- /dev/null
+++ b/src/luarocks/tools/zip.lua
@@ -0,0 +1,531 @@
+
+--- 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 pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+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_uncompress, zlib_crc32
+if zlib._VERSION:match "^lua%-zlib" then
+ 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, 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)
+ return zlib.crc32(zlib.crc32(), data)
+ end
+else
+ error("unknown zlib library", 0)
+end
+
+local function number_to_lestring(number, nbytes)
+ local out = {}
+ for _ = 1, nbytes do
+ local byte = number % 256
+ table.insert(out, string.char(byte))
+ number = (number - byte) / 256
+ end
+ 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.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_open_new_file_in_zip(self, filename)
+ if self.in_open_file then
+ self:close_file_in_zip()
+ return nil
+ end
+ local lfh = {}
+ self.local_file_header = lfh
+ lfh.last_mod_file_time = 0 -- TODO
+ lfh.last_mod_file_date = 0 -- TODO
+ lfh.file_name_length = #filename
+ lfh.extra_field_length = 0
+ lfh.file_name = filename:gsub("\\", "/")
+ lfh.external_attr = shl(493, 16) -- TODO proper permissions
+ self.in_open_file = true
+ return true
+end
+
+--- Write data to the file currently being stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @param data string containing full contents of the file.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_write_file_in_zip(self, data)
+ if not self.in_open_file then
+ return nil
+ end
+ local lfh = self.local_file_header
+ local compressed = zlib_compress(data, "raw")
+ lfh.crc32 = zlib_crc32(data)
+ lfh.compressed_size = #compressed
+ lfh.uncompressed_size = #data
+ self.data = compressed
+ return true
+end
+
+--- Complete the writing of a file stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close_file_in_zip(self)
+ local zh = self.ziphandle
+
+ if not self.in_open_file then
+ return nil
+ end
+
+ -- Local file header
+ local lfh = self.local_file_header
+ lfh.offset = zh:seek()
+ 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(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
+
+ return true
+end
+
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+local function zipwriter_add(self, file)
+ local fin
+ local ok, err = self:open_new_file_in_zip(file)
+ if not ok then
+ err = "error in opening "..file.." in zipfile"
+ else
+ fin = io.open(fs.absolute_name(file), "rb")
+ if not fin then
+ ok = false
+ err = "error opening "..file.." for reading"
+ end
+ end
+ if ok then
+ local data = fin:read("*a")
+ if not data then
+ err = "error reading "..file
+ ok = false
+ else
+ ok = self:write_file_in_zip(data)
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ end
+ if fin then
+ fin:close()
+ end
+ if ok then
+ ok = self:close_file_in_zip()
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ return ok == true, err
+end
+
+--- Complete the writing of the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close(self)
+ local zh = self.ziphandle
+
+ local central_directory_offset = zh:seek()
+
+ local size_of_central_directory = 0
+ -- Central directory structure
+ for _, lfh in ipairs(self.files) do
+ 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(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
+end
+
+--- Return a zip handle open for writing.
+-- @param name filename of the zipfile to be created.
+-- @return a zip handle, or nil in case of error.
+function zip.new_zipwriter(name)
+
+ local zw = {}
+
+ zw.ziphandle = io.open(fs.absolute_name(name), "wb")
+ if not zw.ziphandle then
+ return nil
+ end
+ zw.files = {}
+ zw.in_open_file = false
+
+ zw.add = zipwriter_add
+ zw.close = zipwriter_close
+ zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip
+ zw.write_file_in_zip = zipwriter_write_file_in_zip
+ zw.close_file_in_zip = zipwriter_close_file_in_zip
+
+ return zw
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+function zip.zip(zipfile, ...)
+ local zw = zip.new_zipwriter(zipfile)
+ if not zw then
+ return nil, "error opening "..zipfile
+ end
+
+ local args = pack(...)
+ local ok, err
+ for i=1, args.n do
+ local file = args[i]
+ if fs.is_dir(file) then
+ for _, entry in pairs(fs.find(file)) do
+ local fullname = dir.path(file, entry)
+ if fs.is_file(fullname) then
+ ok, err = zw:add(fullname)
+ if not ok then break end
+ end
+ end
+ else
+ ok, err = zw:add(file)
+ if not ok then break end
+ end
+ end
+
+ zw:close()
+ return ok, err
+end
+
+
+local function ziptime_to_luatime(ztime, zdate)
+ local date = {
+ 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,
+ }
+
+ if date.month == 0 then date.month = 1 end
+ if date.day == 0 then date.day = 1 end
+
+ return date
+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
+
+ -- Skip over the rest of the zip file header. See
+ -- zipwriter_close_file_in_zip for the format.
+ zh:seek("cur", 22)
+ local file_name_length = lestring_to_number(zh:read(2))
+ local extra_field_length = lestring_to_number(zh:read(2))
+ zh:read(file_name_length)
+ zh:read(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 nil, err
+ end
+
+ 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