Skip to content

Commit

Permalink
New zipwriter module (wrapping zlib via ffi)
Browse files Browse the repository at this point in the history
Needed for wikipedia epub generation
  • Loading branch information
poire-z authored and houqp committed Jan 24, 2017
1 parent 243533e commit e8a24fe
Showing 1 changed file with 201 additions and 0 deletions.
201 changes: 201 additions & 0 deletions ffi/zipwriter.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
-- Zip packing workflow & code from luarocks' zip.lua :
-- https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/zip.lua
-- Modified to not require lua-zlib (we can wrap zlib with ffi)
-- cf: http://luajit.org/ext_ffi_tutorial.html, which uses zlib as an example !
-- Simplified to take filename and content from strings and not from disk

-- We only need a few functions from zlib
local ffi = require "ffi"
ffi.cdef([[
unsigned long crc32(unsigned long crc, const char *buf, unsigned len);
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen, const uint8_t *source, unsigned long sourceLen, int level);
]])

-- We only need to wrap 2 zlib functions to make a zip file
local _zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")
local function zlibCompress(data)
local n = _zlib.compressBound(#data)
local buf = ffi.new("uint8_t[?]", n)
local buflen = ffi.new("unsigned long[1]", n)
local res = _zlib.compress2(buf, buflen, data, #data, 9)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
local function zlibCrc32(data, chksum)
chksum = chksum or 0
data = data or ""
return _zlib.crc32(chksum, data, #data)
end

local function numberToByteString(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


-- Pure lua zip writer
local ZipWriter = {}

function ZipWriter:new()
local o = {}
setmetatable(o, self)
self.__index = self
return o
end

--- Begin a new file to be stored inside the zipfile.
function ZipWriter:_open_new_file_in_zip(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 = self.started_time -- 0 = 00:00
lfh.last_mod_file_date = self.started_date -- 0 = 1980-00-00 00:00
lfh.file_name_length = #filename
lfh.extra_field_length = 0
lfh.file_name = filename:gsub("\\", "/")
lfh.external_attr = 0
self.in_open_file = true
return true
end

--- Write data to the file currently being stored in the zipfile.
function ZipWriter:_write_file_in_zip(data)
if not self.in_open_file then
return nil
end
local lfh = self.local_file_header
local compressed = zlibCompress(data):sub(3, -5)
lfh.crc32 = tonumber(zlibCrc32(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.
function ZipWriter:_close_file_in_zip()
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(numberToByteString(0x04034b50, 4)) -- signature
zh:write(numberToByteString(20, 2)) -- version needed to extract: 2.0
zh:write(numberToByteString(0, 2)) -- general purpose bit flag
zh:write(numberToByteString(8, 2)) -- compression method: deflate
zh:write(numberToByteString(lfh.last_mod_file_time, 2))
zh:write(numberToByteString(lfh.last_mod_file_date, 2))
zh:write(numberToByteString(lfh.crc32, 4))
zh:write(numberToByteString(lfh.compressed_size, 4))
zh:write(numberToByteString(lfh.uncompressed_size, 4))
zh:write(numberToByteString(lfh.file_name_length, 2))
zh:write(numberToByteString(lfh.extra_field_length, 2))
zh:write(lfh.file_name)
-- File data
zh:write(self.data)
-- Data descriptor
zh:write(numberToByteString(lfh.crc32, 4))
zh:write(numberToByteString(lfh.compressed_size, 4))
zh:write(numberToByteString(lfh.uncompressed_size, 4))
-- Done, add it to list of files
table.insert(self.files, lfh)
self.in_open_file = false
return true
end

--- Complete the writing of the zipfile.
function ZipWriter:close()
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(numberToByteString(0x02014b50, 4)) -- signature
zh:write(numberToByteString(3, 2)) -- version made by: UNIX
zh:write(numberToByteString(20, 2)) -- version needed to extract: 2.0
zh:write(numberToByteString(0, 2)) -- general purpose bit flag
zh:write(numberToByteString(8, 2)) -- compression method: deflate
zh:write(numberToByteString(lfh.last_mod_file_time, 2))
zh:write(numberToByteString(lfh.last_mod_file_date, 2))
zh:write(numberToByteString(lfh.crc32, 4))
zh:write(numberToByteString(lfh.compressed_size, 4))
zh:write(numberToByteString(lfh.uncompressed_size, 4))
zh:write(numberToByteString(lfh.file_name_length, 2))
zh:write(numberToByteString(lfh.extra_field_length, 2))
zh:write(numberToByteString(0, 2)) -- file comment length
zh:write(numberToByteString(0, 2)) -- disk number start
zh:write(numberToByteString(0, 2)) -- internal file attributes
zh:write(numberToByteString(lfh.external_attr, 4)) -- external file attributes
zh:write(numberToByteString(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(numberToByteString(0x06054b50, 4)) -- signature
zh:write(numberToByteString(0, 2)) -- number of this disk
zh:write(numberToByteString(0, 2)) -- number of disk with start of central directory
zh:write(numberToByteString(#self.files, 2)) -- total number of entries in the central dir on this disk
zh:write(numberToByteString(#self.files, 2)) -- total number of entries in the central dir
zh:write(numberToByteString(size_of_central_directory, 4))
zh:write(numberToByteString(central_directory_offset, 4))
zh:write(numberToByteString(0, 2)) -- zip file comment length
zh:close()
return true
end

-- Open zipfile
function ZipWriter:open(zipfilepath)
self.files = {}
self.in_open_file = false
-- set modification date and time of files to now
local t = os.date("*t")
self.started_date = bit.bor(
bit.lshift(t.year-1980, 9),
bit.lshift(t.month, 5),
bit.lshift(t.day, 0)
)
self.started_time = bit.bor(
bit.lshift(t.hour, 11),
bit.lshift(t.min, 5),
bit.rshift(t.sec+2, 1)
)
self.ziphandle = io.open(zipfilepath, "wb")
if not self.ziphandle then
return nil
end
return true
end

-- Add to zipfile content with the name in_zip_filepath
function ZipWriter:add(in_zip_filepath, content)
self:_open_new_file_in_zip(in_zip_filepath)
self:_write_file_in_zip(content)
self:_close_file_in_zip()
end

-- Convenience function
-- function ZipWriter.createZipWithFiles(zipfilename, files)
-- local zw = ZipWriter:new()
-- zw:open(zipfilename)
-- for _, f in pairs(files) do
-- zw:add(f.filename, f.content)
-- end
-- zw:close()
-- end
-- files = {}
-- files[1] = {filename="tutu.txt", content="this is tutu"}
-- files[2] = {filename="subtoto/toto.txt", content="this is toto in subtoto directory"}
-- createZipWithFiles("tata.zip", files)

return ZipWriter

0 comments on commit e8a24fe

Please sign in to comment.