1
2--- A Lua implementation of .zip and .gz file compression and decompression,
3-- using only lzlib or lua-lzib.
4local zip = {}
5
6local zlib = require("zlib")
7local fs = require("luarocks.fs")
8local fun = require("luarocks.fun")
9local dir = require("luarocks.dir")
10
11local pack = table.pack or function(...) return { n = select("#", ...), ... } end
12
13local function shr(n, m)
14   return math.floor(n / 2^m)
15end
16
17local function shl(n, m)
18   return n * 2^m
19end
20local function lowbits(n, m)
21   return n % 2^m
22end
23
24local function mode_to_windowbits(mode)
25   if mode == "gzip" then
26      return 31
27   elseif mode == "zlib" then
28      return 0
29   elseif mode == "raw" then
30      return -15
31   end
32end
33
34-- zlib module can be provided by both lzlib and lua-lzib packages.
35-- Create a compatibility layer.
36local zlib_compress, zlib_uncompress, zlib_crc32
37if zlib._VERSION:match "^lua%-zlib" then
38   function zlib_compress(data, mode)
39      return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish"))
40   end
41
42   function zlib_uncompress(data, mode)
43      return (zlib.inflate(mode_to_windowbits(mode))(data))
44   end
45
46   function zlib_crc32(data)
47      return zlib.crc32()(data)
48   end
49elseif zlib._VERSION:match "^lzlib" then
50   function zlib_compress(data, mode)
51      return zlib.compress(data, -1, nil, mode_to_windowbits(mode))
52   end
53
54   function zlib_uncompress(data, mode)
55      return zlib.decompress(data, mode_to_windowbits(mode))
56   end
57
58   function zlib_crc32(data)
59      return zlib.crc32(zlib.crc32(), data)
60   end
61else
62   error("unknown zlib library", 0)
63end
64
65local function number_to_lestring(number, nbytes)
66   local out = {}
67   for _ = 1, nbytes do
68      local byte = number % 256
69      table.insert(out, string.char(byte))
70      number = (number - byte) / 256
71   end
72   return table.concat(out)
73end
74
75local function lestring_to_number(str)
76   local n = 0
77   local bytes = { string.byte(str, 1, #str) }
78   for b = 1, #str do
79      n = n + shl(bytes[b], (b-1)*8)
80   end
81   return math.floor(n)
82end
83
84local LOCAL_FILE_HEADER_SIGNATURE  = number_to_lestring(0x04034b50, 4)
85local DATA_DESCRIPTOR_SIGNATURE    = number_to_lestring(0x08074b50, 4)
86local CENTRAL_DIRECTORY_SIGNATURE  = number_to_lestring(0x02014b50, 4)
87local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4)
88
89--- Begin a new file to be stored inside the zipfile.
90-- @param self handle of the zipfile being written.
91-- @param filename filenome of the file to be added to the zipfile.
92-- @return true if succeeded, nil in case of failure.
93local function zipwriter_open_new_file_in_zip(self, filename)
94   if self.in_open_file then
95      self:close_file_in_zip()
96      return nil
97   end
98   local lfh = {}
99   self.local_file_header = lfh
100   lfh.last_mod_file_time = 0 -- TODO
101   lfh.last_mod_file_date = 0 -- TODO
102   lfh.file_name_length = #filename
103   lfh.extra_field_length = 0
104   lfh.file_name = filename:gsub("\\", "/")
105   lfh.external_attr = shl(493, 16) -- TODO proper permissions
106   self.in_open_file = true
107   return true
108end
109
110--- Write data to the file currently being stored in the zipfile.
111-- @param self handle of the zipfile being written.
112-- @param data string containing full contents of the file.
113-- @return true if succeeded, nil in case of failure.
114local function zipwriter_write_file_in_zip(self, data)
115   if not self.in_open_file then
116      return nil
117   end
118   local lfh = self.local_file_header
119   local compressed = zlib_compress(data, "raw")
120   lfh.crc32 = zlib_crc32(data)
121   lfh.compressed_size = #compressed
122   lfh.uncompressed_size = #data
123   self.data = compressed
124   return true
125end
126
127--- Complete the writing of a file stored in the zipfile.
128-- @param self handle of the zipfile being written.
129-- @return true if succeeded, nil in case of failure.
130local function zipwriter_close_file_in_zip(self)
131   local zh = self.ziphandle
132
133   if not self.in_open_file then
134      return nil
135   end
136
137   -- Local file header
138   local lfh = self.local_file_header
139   lfh.offset = zh:seek()
140   zh:write(LOCAL_FILE_HEADER_SIGNATURE)
141   zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
142   zh:write(number_to_lestring(4, 2)) -- general purpose bit flag
143   zh:write(number_to_lestring(8, 2)) -- compression method: deflate
144   zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
145   zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
146   zh:write(number_to_lestring(lfh.crc32, 4))
147   zh:write(number_to_lestring(lfh.compressed_size, 4))
148   zh:write(number_to_lestring(lfh.uncompressed_size, 4))
149   zh:write(number_to_lestring(lfh.file_name_length, 2))
150   zh:write(number_to_lestring(lfh.extra_field_length, 2))
151   zh:write(lfh.file_name)
152
153   -- File data
154   zh:write(self.data)
155
156   -- Data descriptor
157   zh:write(DATA_DESCRIPTOR_SIGNATURE)
158   zh:write(number_to_lestring(lfh.crc32, 4))
159   zh:write(number_to_lestring(lfh.compressed_size, 4))
160   zh:write(number_to_lestring(lfh.uncompressed_size, 4))
161
162   table.insert(self.files, lfh)
163   self.in_open_file = false
164
165   return true
166end
167
168-- @return boolean or (boolean, string): true on success,
169-- false and an error message on failure.
170local function zipwriter_add(self, file)
171   local fin
172   local ok, err = self:open_new_file_in_zip(file)
173   if not ok then
174      err = "error in opening "..file.." in zipfile"
175   else
176      fin = io.open(fs.absolute_name(file), "rb")
177      if not fin then
178         ok = false
179         err = "error opening "..file.." for reading"
180      end
181   end
182   if ok then
183      local data = fin:read("*a")
184      if not data then
185         err = "error reading "..file
186         ok = false
187      else
188         ok = self:write_file_in_zip(data)
189         if not ok then
190            err = "error in writing "..file.." in the zipfile"
191         end
192      end
193   end
194   if fin then
195      fin:close()
196   end
197   if ok then
198      ok = self:close_file_in_zip()
199      if not ok then
200         err = "error in writing "..file.." in the zipfile"
201      end
202   end
203   return ok == true, err
204end
205
206--- Complete the writing of the zipfile.
207-- @param self handle of the zipfile being written.
208-- @return true if succeeded, nil in case of failure.
209local function zipwriter_close(self)
210   local zh = self.ziphandle
211
212   local central_directory_offset = zh:seek()
213
214   local size_of_central_directory = 0
215   -- Central directory structure
216   for _, lfh in ipairs(self.files) do
217      zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature
218      zh:write(number_to_lestring(3, 2)) -- version made by: UNIX
219      zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
220      zh:write(number_to_lestring(0, 2)) -- general purpose bit flag
221      zh:write(number_to_lestring(8, 2)) -- compression method: deflate
222      zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
223      zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
224      zh:write(number_to_lestring(lfh.crc32, 4))
225      zh:write(number_to_lestring(lfh.compressed_size, 4))
226      zh:write(number_to_lestring(lfh.uncompressed_size, 4))
227      zh:write(number_to_lestring(lfh.file_name_length, 2))
228      zh:write(number_to_lestring(lfh.extra_field_length, 2))
229      zh:write(number_to_lestring(0, 2)) -- file comment length
230      zh:write(number_to_lestring(0, 2)) -- disk number start
231      zh:write(number_to_lestring(0, 2)) -- internal file attributes
232      zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes
233      zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header
234      zh:write(lfh.file_name)
235      size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length
236   end
237
238   -- End of central directory record
239   zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature
240   zh:write(number_to_lestring(0, 2)) -- number of this disk
241   zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory
242   zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk
243   zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir
244   zh:write(number_to_lestring(size_of_central_directory, 4))
245   zh:write(number_to_lestring(central_directory_offset, 4))
246   zh:write(number_to_lestring(0, 2)) -- zip file comment length
247   zh:close()
248
249   return true
250end
251
252--- Return a zip handle open for writing.
253-- @param name filename of the zipfile to be created.
254-- @return a zip handle, or nil in case of error.
255function zip.new_zipwriter(name)
256
257   local zw = {}
258
259   zw.ziphandle = io.open(fs.absolute_name(name), "wb")
260   if not zw.ziphandle then
261      return nil
262   end
263   zw.files = {}
264   zw.in_open_file = false
265
266   zw.add = zipwriter_add
267   zw.close = zipwriter_close
268   zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip
269   zw.write_file_in_zip = zipwriter_write_file_in_zip
270   zw.close_file_in_zip = zipwriter_close_file_in_zip
271
272   return zw
273end
274
275--- Compress files in a .zip archive.
276-- @param zipfile string: pathname of .zip archive to be created.
277-- @param ... Filenames to be stored in the archive are given as
278-- additional arguments.
279-- @return boolean or (boolean, string): true on success,
280-- false and an error message on failure.
281function zip.zip(zipfile, ...)
282   local zw = zip.new_zipwriter(zipfile)
283   if not zw then
284      return nil, "error opening "..zipfile
285   end
286
287   local args = pack(...)
288   local ok, err
289   for i=1, args.n do
290      local file = args[i]
291      if fs.is_dir(file) then
292         for _, entry in pairs(fs.find(file)) do
293            local fullname = dir.path(file, entry)
294            if fs.is_file(fullname) then
295               ok, err = zw:add(fullname)
296               if not ok then break end
297            end
298         end
299      else
300         ok, err = zw:add(file)
301         if not ok then break end
302      end
303   end
304
305   zw:close()
306   return ok, err
307end
308
309
310local function ziptime_to_luatime(ztime, zdate)
311   local date = {
312      year = shr(zdate, 9) + 1980,
313      month = shr(lowbits(zdate, 9), 5),
314      day = lowbits(zdate, 5),
315      hour = shr(ztime, 11),
316      min = shr(lowbits(ztime, 11), 5),
317      sec = lowbits(ztime, 5) * 2,
318   }
319
320   if date.month == 0 then date.month = 1 end
321   if date.day == 0 then date.day = 1 end
322
323   return date
324end
325
326local function read_file_in_zip(zh, cdr)
327   local sig = zh:read(4)
328   if sig ~= LOCAL_FILE_HEADER_SIGNATURE then
329      return nil, "failed reading Local File Header signature"
330   end
331
332   local lfh = {}
333   lfh.version_needed = lestring_to_number(zh:read(2))
334   lfh.bitflag = lestring_to_number(zh:read(2))
335   lfh.compression_method = lestring_to_number(zh:read(2))
336   lfh.last_mod_file_time = lestring_to_number(zh:read(2))
337   lfh.last_mod_file_date = lestring_to_number(zh:read(2))
338   lfh.crc32 = lestring_to_number(zh:read(4))
339   lfh.compressed_size = lestring_to_number(zh:read(4))
340   lfh.uncompressed_size = lestring_to_number(zh:read(4))
341   lfh.file_name_length = lestring_to_number(zh:read(2))
342   lfh.extra_field_length = lestring_to_number(zh:read(2))
343   lfh.file_name = zh:read(lfh.file_name_length)
344   lfh.extra_field = zh:read(lfh.extra_field_length)
345
346   local data = zh:read(cdr.compressed_size)
347
348   local uncompressed
349   if cdr.compression_method == 8 then
350      uncompressed = zlib_uncompress(data, "raw")
351   elseif cdr.compression_method == 0 then
352      uncompressed = data
353   else
354      return nil, "unknown compression method " .. cdr.compression_method
355   end
356
357   if #uncompressed ~= cdr.uncompressed_size then
358      return nil, "uncompressed size doesn't match"
359   end
360   if cdr.crc32 ~= zlib_crc32(uncompressed) then
361      return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed
362   end
363
364   return uncompressed
365end
366
367local function process_end_of_central_dir(zh)
368   local at, err = zh:seek("end", -22)
369   if not at then
370      return nil, err
371   end
372
373   while true do
374      local sig = zh:read(4)
375      if sig == END_OF_CENTRAL_DIR_SIGNATURE then
376         break
377      end
378      at = at - 1
379      local at1, err = zh:seek("set", at)
380      if at1 ~= at then
381         return nil, "Could not find End of Central Directory signature"
382      end
383   end
384
385   -- number of this disk (2 bytes)
386   -- number of the disk with the start of the central directory (2 bytes)
387   -- total number of entries in the central directory on this disk (2 bytes)
388   -- total number of entries in the central directory (2 bytes)
389   zh:seek("cur", 6)
390
391   local central_directory_entries = lestring_to_number(zh:read(2))
392
393   -- central directory size (4 bytes)
394   zh:seek("cur", 4)
395
396   local central_directory_offset = lestring_to_number(zh:read(4))
397
398   return central_directory_entries, central_directory_offset
399end
400
401local function process_central_dir(zh, cd_entries)
402
403   local files = {}
404
405   for i = 1, cd_entries do
406      local sig = zh:read(4)
407      if sig ~= CENTRAL_DIRECTORY_SIGNATURE then
408         return nil, "failed reading Central Directory signature"
409      end
410
411      local cdr = {}
412      files[i] = cdr
413
414      cdr.version_made_by = lestring_to_number(zh:read(2))
415      cdr.version_needed = lestring_to_number(zh:read(2))
416      cdr.bitflag = lestring_to_number(zh:read(2))
417      cdr.compression_method = lestring_to_number(zh:read(2))
418      cdr.last_mod_file_time = lestring_to_number(zh:read(2))
419      cdr.last_mod_file_date = lestring_to_number(zh:read(2))
420      cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date)
421      cdr.crc32 = lestring_to_number(zh:read(4))
422      cdr.compressed_size = lestring_to_number(zh:read(4))
423      cdr.uncompressed_size = lestring_to_number(zh:read(4))
424      cdr.file_name_length = lestring_to_number(zh:read(2))
425      cdr.extra_field_length = lestring_to_number(zh:read(2))
426      cdr.file_comment_length = lestring_to_number(zh:read(2))
427      cdr.disk_number_start = lestring_to_number(zh:read(2))
428      cdr.internal_attr = lestring_to_number(zh:read(2))
429      cdr.external_attr = lestring_to_number(zh:read(4))
430      cdr.offset = lestring_to_number(zh:read(4))
431      cdr.file_name = zh:read(cdr.file_name_length)
432      cdr.extra_field = zh:read(cdr.extra_field_length)
433      cdr.file_comment = zh:read(cdr.file_comment_length)
434   end
435   return files
436end
437
438--- Uncompress files from a .zip archive.
439-- @param zipfile string: pathname of .zip archive to be created.
440-- @return boolean or (boolean, string): true on success,
441-- false and an error message on failure.
442function zip.unzip(zipfile)
443   zipfile = fs.absolute_name(zipfile)
444   local zh, err = io.open(zipfile, "rb")
445   if not zh then
446      return nil, err
447   end
448
449   local cd_entries, cd_offset = process_end_of_central_dir(zh)
450   if not cd_entries then
451      return nil, cd_offset
452   end
453
454   local ok, err = zh:seek("set", cd_offset)
455   if not ok then
456      return nil, err
457   end
458
459   local files, err = process_central_dir(zh, cd_entries)
460   if not files then
461      return nil, err
462   end
463
464   for _, cdr in ipairs(files) do
465      local file = cdr.file_name
466      if file:sub(#file) == "/" then
467         local ok, err = fs.make_dir(dir.path(fs.current_dir(), file))
468         if not ok then
469            return nil, err
470         end
471      else
472         local base = dir.dir_name(file)
473         if base ~= "" then
474            base = dir.path(fs.current_dir(), base)
475            if not fs.is_dir(base) then
476               local ok, err = fs.make_dir(base)
477               if not ok then
478                  return nil, err
479               end
480            end
481         end
482
483         local ok, err = zh:seek("set", cdr.offset)
484         if not ok then
485            return nil, err
486         end
487
488         local contents, err = read_file_in_zip(zh, cdr)
489         if not contents then
490            return nil, err
491         end
492         local pathname = dir.path(fs.current_dir(), file)
493         local wf, err = io.open(pathname, "wb")
494         if not wf then
495            zh:close()
496            return nil, err
497         end
498         wf:write(contents)
499         wf:close()
500
501         if cdr.external_attr > 0 then
502            fs.set_permissions(pathname, "exec", "all")
503         else
504            fs.set_permissions(pathname, "read", "all")
505         end
506         fs.set_time(pathname, cdr.last_mod_luatime)
507      end
508   end
509   zh:close()
510   return true
511end
512
513function zip.gzip(input_filename, output_filename)
514   assert(type(input_filename) == "string")
515   assert(output_filename == nil or type(output_filename) == "string")
516
517   if not output_filename then
518      output_filename = input_filename .. ".gz"
519   end
520
521   local fn = fun.partial(fun.flip(zlib_compress), "gzip")
522   return fs.filter_file(fn, input_filename, output_filename)
523end
524
525function zip.gunzip(input_filename, output_filename)
526   assert(type(input_filename) == "string")
527   assert(output_filename == nil or type(output_filename) == "string")
528
529   if not output_filename then
530      output_filename = input_filename:gsub("%.gz$", "")
531   end
532
533   local fn = fun.partial(fun.flip(zlib_uncompress), "gzip")
534   return fs.filter_file(fn, input_filename, output_filename)
535end
536
537return zip
538