-
Notifications
You must be signed in to change notification settings - Fork 6
/
zip.lua
596 lines (503 loc) · 18.1 KB
/
zip.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
--[=[
Extracting and creating zip archives (DEFLATE only).
Written by Cosmin Apreutesei. Public Domain.
FEATURES
* reading and writing zip archives from memory.
* password protection with AES encryption.
* preserving file attributes and timestamps across file systems.
* multi-file archives.
* following and storing symbolic links.
* utf8 filename support.
* zipping of central directory to reduce size.
* generate and verify CMS file signatures.
* recover the central directory if it is corrupt or missing.
LIMITATIONS
* no LZMA or bzip2 (the binding supports it but the binary doesn't).
* no stream API (use temp files, it's ok).
BROWSING
[try_]zip_open(opt | file,[mode],[passwd]) -> rz|wz
mode : open mode ('r'; 'r'|'w'|'a')
file : open zip file from disk
in_memory : load whole file in memory (false)
data : open zip file from buffer or string
size : data size (#data)
copy : copy the buffer before loading (false)
pattern : filter listing entries
ci_pattern : filter listing entries (case insensitive)
password : set password (for both rz and wz)
raw : set raw mode (for both rz and wz)
encoding : support codepages in filenames (;'utf8'|codepage)
zip_cd : zip the central directory (false)
aes : use aes encryption (true !!!)
store_links : store symlinks (false)
follow_links : follow symlinks (false)
compression_level : compression level (9; 0..9)
compression_method : compression method ('deflate'; 'store'|'deflate')
rz:entries() -> iter() -> e iterate entries
rz:first() -> t|f goto first entry
rz:next() -> t|f goto next entry
rz:find(filename[, ignore_case]) -> t|f find entry
rz.entry_is_dir -> t|f is current entry a directory?
rz:entry_hash(['md5'|'sha1'|'sha256']) -> s|false current entry hash
rz.sign_required = t|f require signing
rz.file_has_sign -> t|f is _opened *file* entry_ signed?
rz:file_verify_sign() -> t|f verify signature of _opened file entry_
rz.entry -> e get current entry info
e.compression_method -> s - compression method
e.mtime -> ts - last modified time
e.atime -> ts - last accessed time
e.btime -> ts - creation time
e.crc -> n - crc-32
e.compressed_size -> n - compressed size
e.uncompressed_size -> n - uncompressed size
e.disk_number -> n - disk number start
e.disk_offset -> n - relative offset of local header
e.internal_fa -> n - internal file attributes
e.external_fa -> n - external file attributes
e.filename -> s - filename
e.comment -> s - comment
e.linkname -> s - sym-link filename
e.zip64 -> t|f - zip64 extension mode
e.aes_version -> n - winzip aes extension if not 0
e.aes_strength -> n - winzip aes encryption strength
rz.pattern = s filter listing entries
rz.ci_pattern = s filter listing entries (case insensitive)
rz|wz.password = s set password for decryption/encryption
rz|wz.raw = t|f set raw mode
rz|wz.raw -> t|f get raw mode
rz|wz.zip_handle -> z get C zip handle
rz|wz:close() close the zip file
EXTRACTION
rz:extract(to_filepath) extract current entry to file
rz:extract_all(to_dir) extract all to dir
rz:read'*a' -> s read entire entry as string
rz:open_entry() open current entry
rz:read(buf, maxlen) -> len read from opened entry into a buffer
rz:close_entry() close entry
rz.encoding = 'utf8'|codepage support codepages in filenames
rz.zip_cd -> t|f does the zip have a zipped central directory?
rz.comment -> s get comment for the central directory
COMPRESSION
wz.zip_cd = t|f zip the central directory
wz.aes = t|f use aes encryption
wz.store_links = t|f store symlinks
wz.follow_links = t|f follow symlinks
wz.compression_level = 0..9 set compression level
wz.compression_method = 'store|deflate' set compression method
wz:add_file(filepath[, filepath_in_zip]) archive a file
wz:add_memfile{filename=,data=,[size=],...} add a file from a memory buffer
wz:add_all(dir,[root_dir],[incl_path],[recursive]) add entire dir
wz:add_all_from_zip(rz) add all entries from other zip file
wz:zip_cd() compress central directory
wz:set_cert(cert_path[, password]) set signing certificate
All functions raise on errors, with the exception of I/O and parsing errors
on which they return `nil, err, errcode`.
Neither Windows Explorer on Windows 10 nor Total Commander can read zip files
with zipped central directory (`zip_cd` option).
Windows Explorer on Windows 10 cannot read AES-encrypted zip entries
(`aes` option, enabled by default). On the other hand, the old PKZIP
encryption (`aes = false`) is not secure at all, and can be decrypted with
specialized tools since 1990 regardless of password length. So you have
to choose between security and accessibility with this one as you can't
have both. AES encryption (`aes` option) means AES-256, the only option.
]=]
if not ... then require'zip_test'; return end
require'glue'
require'minizip2_h'
require'minizip2_rw_h'
local C = ffi.load'minizip2'
--tools ----------------------------------------------------------------------
local function init_properties(self, t, fields)
for k in pairs(fields) do
if t[k] then self[k] = t[k] end
end
end
--zip entry ------------------------------------------------------------------
local entry_get = {}
local entry_set = {}
function entry_get:filename () return str(self.filename_ptr , self.filename_size) end
function entry_get:comment () return str(self.comment_ptr , self.comment_size ) end
function entry_get:linkname () return str(self.linkname_ptr ) end
function entry_set:filename (s) self.filename_ptr = s; self.filename_size = #s end
function entry_set:comment (s) self.comment_ptr = s; self.comment_size = #s end
function entry_set:linkname (s) self.linkname_ptr = s; end
local compression_methods = {
store = C.MZ_COMPRESS_METHOD_STORE ,
deflate = C.MZ_COMPRESS_METHOD_DEFLATE,
bzip2 = C.MZ_COMPRESS_METHOD_BZIP2 ,
lzma = C.MZ_COMPRESS_METHOD_LZMA ,
aes = C.MZ_COMPRESS_METHOD_AES ,
}
local compression_method_names = index(compression_methods)
function entry_get:compression_method()
return compression_method_names[self.compression_method_num]
end
local aes_bits = {
[C.MZ_AES_STRENGTH_128] = 128,
[C.MZ_AES_STRENGTH_192] = 192,
[C.MZ_AES_STRENGTH_256] = 256,
}
function entry_get:aes_bits()
return aes_bits[self.aes_strength]
end
function entry_get:mtime() return tonumber(self.mtime_t) end
function entry_get:atime() return tonumber(self.atime_t) end
function entry_get:btime() return tonumber(self.btime_t) end
function entry_set:mtime(t) self.mtime_t = t end
function entry_set:atime(t) self.atime_t = t end
function entry_set:btime(t) self.btime_t = t end
function entry_get:compressed_size () return tonumber(self.compressed_size_i64 ) end
function entry_get:uncompressed_size () return tonumber(self.uncompressed_size_i64) end
function entry_get:disk_offset () return tonumber(self.disk_offset_i64 ) end
function entry_set:compressed_size (n) self.compressed_size_i64 = n end
function entry_set:uncompressed_size (n) self.uncompressed_size_i64 = n end
function entry_set:disk_offset (n) self.disk_offset_i64 = n end
function entry_get:zip64 () return self.zip64_u16 == 1 end
function entry_set:zip64 (b) self.zip64_u16 = b end
metatype('mz_zip_file', gettersandsetters(entry_get, entry_set))
--reader & writer ------------------------------------------------------------
cdef[[
typedef struct minizip_reader_t;
typedef struct minizip_writer_t;
]]
local reader_ptr_ct = ctype'struct minizip_reader_t*'
local writer_ptr_ct = ctype'struct minizip_writer_t*'
local reader = {}; local reader_get = {}; local reader_set = {}
local writer = {}; local writer_get = {}; local writer_set = {}
local cbuf = new'char*[1]'
local bbuf = new'uint8_t[1]'
local vbuf = new'void*[1]'
local entry_init_fields = {
mtime=1,
atime=1,
btime=1,
filename=1,
comment=1,
linkname=1,
}
local function mz_zip_file(t)
local e = new'mz_zip_file'
init_properties(e, t, entry_init_fields)
gc(e, function() --anchor the strings
local _ = t.filename
local _ = t.comment
local _ = t.linkname
end)
return e
end
local error_strings = {
[C.MZ_DATA_ERROR ] = 'data',
[C.MZ_END_OF_STREAM ] = 'eof',
[C.MZ_CRC_ERROR ] = 'crc',
[C.MZ_CRYPT_ERROR ] = 'crypt',
[C.MZ_PASSWORD_ERROR ] = 'password',
[C.MZ_SUPPORT_ERROR ] = 'support',
[C.MZ_HASH_ERROR ] = 'hash',
[C.MZ_OPEN_ERROR ] = 'open',
[C.MZ_EXIST_ERROR ] = 'exist',
[C.MZ_CLOSE_ERROR ] = 'close',
[C.MZ_SEEK_ERROR ] = 'seek',
[C.MZ_TELL_ERROR ] = 'tell',
[C.MZ_READ_ERROR ] = 'read',
[C.MZ_WRITE_ERROR ] = 'write',
[C.MZ_SIGN_ERROR ] = 'sign',
[C.MZ_SYMLINK_ERROR ] = 'symlink',
}
local function check(err, ret)
assert(err ~= C.MZ_PARAM_ERROR , 'param error')
assert(err ~= C.MZ_INTERNAL_ERROR , 'internal error')
assert(err ~= C.MZ_STREAM_ERROR , 'stream error')
assert(err ~= C.MZ_MEM_ERROR , 'memory error')
assert(err ~= C.MZ_BUF_ERROR , 'buffer error')
assert(err ~= C.MZ_VERSION_ERROR , 'version error')
if err >= 0 then return ret end
local err = error_strings[err] or err
return nil, format('minizip %s error', err), err
end
local function checkok(err)
return check(err, true)
end
local function checklen(err)
return check(err, err > 0 and err or nil)
end
local function open_reader(t)
vbuf[0] = C.mz_zip_reader_create()
assert(vbuf[0] ~= nil)
local z = cast(reader_ptr_ct, vbuf[0])
init_properties(z, t, reader_set)
local err
if t.file then
if t.in_memory then
err = C.mz_zip_reader_open_file_in_memory(z, t.file)
else
err = C.mz_zip_reader_open_file(z, t.file)
end
elseif t.data then
err = C.mz_zip_reader_open_buffer(z, t.data, t.size or #t.data, t.copy or false)
gc(z, function() local _ = t.data end) --anchor it
else
--TODO: int32_t mz_zip_reader_open(void *handle, void *stream);
assert(false)
end
if err ~= 0 then
C.mz_zip_reader_delete(vbuf)
end
return check(err, z)
end
local function open_writer(t)
vbuf[0] = C.mz_zip_writer_create()
assert(vbuf[0] ~= nil)
local z = cast(writer_ptr_ct, vbuf[0])
init_properties(z, t, writer_set)
local err
if t.file then
err = C.mz_zip_writer_open_file(z, t.file, t.disk_size or 0, t.mode == 'a')
else
--TODO: int32_t mz_zip_writer_open(void *handle, void *stream, uint8_t append);
assert(false)
end
if err ~= 0 then
C.mz_zip_writer_delete(vbuf)
end
return check(err, z)
end
function try_zip_open(t, mode, password)
if isstr(t) then
t = {file = t, mode = mode, password = password}
end
local open = (t.mode or 'r') == 'r' and open_reader or open_writer
return open(t)
end
function zip_open(...)
return assert(try_zip_open(...))
end
function reader:close()
local ok, err, errcode = checkok(C.mz_zip_reader_close(self))
vbuf[0] = self
C.mz_zip_reader_delete(vbuf)
if not ok then return nil, err, errcode end
return true
end
function writer:close()
local ok, err, errcode = checkok(C.mz_zip_writer_close(self))
vbuf[0] = self
C.mz_zip_writer_delete(vbuf)
if not ok then return nil, err, errcode end
return true
end
--reader entry catalog
local function checkeol(err)
if err == C.MZ_END_OF_LIST then return false end
return checkok(err)
end
function reader:first()
return checkeol(C.mz_zip_reader_goto_first_entry(self))
end
function reader:next()
return checkeol(C.mz_zip_reader_goto_next_entry(self))
end
function reader:find(filename, ignore_case)
return checkeol(C.mz_zip_reader_locate_entry(self, filename, ignore_case or false))
end
local pebuf = new'mz_zip_file*[1]'
function reader_get:entry()
assert(checkok(C.mz_zip_reader_entry_get_info(self, pebuf)))
return pebuf[0]
end
function reader:entries()
return function(self, e)
if e == false then return nil end
local ok, err, ec
if e == nil then
ok, err, ec = self:first()
else
ok, err, ec = self:next()
end
if ok == nil then return false, err, ec end
if ok == false then return nil end
local e, err, ec = self.entry
if not e then return false, err, ec end
return e
end, self
end
function reader_set:pattern(pattern)
C.mz_zip_reader_set_pattern(self, pattern, false)
end
function reader_set:ci_pattern(pattern)
C.mz_zip_reader_set_pattern(self, pattern, true)
end
function reader_set:encoding(encoding)
if encoding == 'utf8' then encoding = C.MZ_ENCODING_UTF8 end
C.mz_zip_reader_set_encoding(self, encoding)
end
function reader_get:comment()
assert(checkok(C.mz_zip_reader_get_comment(self, cbuf)))
return str(cbuf[0])
end
function reader_get:zip_cd()
assert(checkok(C.mz_zip_reader_get_zip_cd(self, bbuf)))
return bbuf[0] == 1
end
--reader entry I/O
function reader:open_entry()
return checkok(C.mz_zip_reader_entry_open(self))
end
function reader:read(buf, len)
if buf == '*a' then --NOTE: 2GB max this way!
local len, err = checklen(C.mz_zip_reader_entry_save_buffer_length(self))
if not len then
if err then return nil, err end
return nil
end
local buf = u8a(len)
local ok, err, ec = checkok(C.mz_zip_reader_entry_save_buffer(self, buf, len))
if not ok then return nil, err, ec end
return str(buf, len)
else
return checklen(C.mz_zip_reader_entry_read(self, buf, len))
end
end
function reader:close_entry()
return checkok(C.mz_zip_reader_entry_close(self))
end
function reader:extract(dest_file)
return checkok(C.mz_zip_reader_entry_save_file(self, dest_file))
end
function reader:extract_all(dest_dir)
return checkok(C.mz_zip_reader_save_all(self, dest_dir))
end
local function assert_checkexist(err)
if err == C.MZ_EXIST_ERROR then return false end
return assert(check(err, true))
end
function reader_get:entry_is_dir()
return assert_checkexist(C.mz_zip_reader_entry_is_dir(self))
end
local algorithms = {
md5 = C.MZ_HASH_MD5 ,
sha1 = C.MZ_HASH_SHA1 ,
sha256 = C.MZ_HASH_SHA256,
}
local digest_sizes = {
md5 = C.MZ_HASH_MD5_SIZE ,
sha1 = C.MZ_HASH_SHA1_SIZE ,
sha256 = C.MZ_HASH_SHA256_SIZE,
}
function reader:entry_hash(algorithm, hbuf, hbuf_size)
algorithm = algorithm or 'sha256'
local digest_size = assert(digest_sizes[algorithm])
local algorithm = assert(algorithms[algorithm])
local return_string
if not hbuf then
return_string = true
hbuf_size = digest_size
hbuf = u8a(digest_size)
elseif hbuf_size < digest_size then
return nil, digest_size
end
local exists = assert_checkexist(C.mz_zip_reader_entry_get_hash(
self, algorithm, hbuf, digest_size))
if return_string then
return str(hbuf, digest_size)
end
return exists
end
function reader_set:sign_required(req)
C.mz_zip_reader_set_sign_required(self, req and true or false)
end
function reader:file_verify_sign()
return assert_checkexist(C.mz_zip_reader_entry_sign_verify(self))
end
function reader_set:password(password)
C.mz_zip_reader_set_password(self, password)
end
function reader_set:raw(raw)
C.mz_zip_reader_set_raw(self, raw)
end
function reader_get:raw()
assert(checkok(C.mz_zip_reader_get_raw(self, bbuf)))
return bbuf[0] == 1
end
function reader_get:zip_handle()
assert(checkok(C.mz_zip_reader_get_zip_handle(self, vbuf)))
return vbuf[0]
end
--writer entry catalog & I/O
function writer:add_entry(entry)
return checkok(C.mz_zip_writer_add_info(self, nil, nil, mz_zip_file(entry)))
end
function writer:write(buf, len)
return checklen(C.mz_zip_writer_entry_write(self, buf, len or #buf))
end
function writer:close_entry()
return checkok(C.mz_zip_writer_entry_close(self))
end
function writer:add_file(file, filename_in_zip)
return checkok(C.mz_zip_writer_add_file(self, file, filename_in_zip))
end
local void_ptr_ct = ctype'void*'
function writer:add_memfile(entry, ...)
if isstr(entry) then
local data, size = ...
entry = {filename = entry, data = data, size = size}
end
return checkok(C.mz_zip_writer_add_buffer(self,
cast(void_ptr_ct, entry.data),
entry.size or #entry.data,
mz_zip_file(entry)))
end
function writer:add_all(dir, root_dir, include_path, recursive)
return checkok(C.mz_zip_writer_add_path(self, dir, root_dir,
include_path or false,
recursive ~= false))
end
function writer:add_all_from_zip(reader)
return checkok(C.mz_zip_writer_copy_from_reader(self, reader))
end
function writer:zip_cd()
assert(check(C.mz_zip_writer_zip_cd(), true))
end
function writer_set:password(password)
C.mz_zip_writer_set_password(self, password)
end
function writer_set:comment(comment)
C.mz_zip_writer_set_comment(self, comment)
end
function writer_set:raw(raw)
C.mz_zip_writer_set_raw(self, raw and true or false)
end
function writer_get:raw()
assert(checkok(C.mz_zip_writer_get_raw(self, bbuf)))
return bbuf[0] == 1
end
function writer_set:aes(aes)
C.mz_zip_writer_set_aes(self, aes and true or false)
end
function writer_set:compression_method(s)
C.mz_zip_writer_set_compress_method(self, compression_methods[s])
end
function writer_set:compression_level(level)
if level <= 0 then
self.compression_method = 'store'
else
C.mz_zip_writer_set_compress_level(self, clamp(level, 1, 9))
end
end
function writer_set:follow_links(follow)
C.mz_zip_writer_set_follow_links(self, follow and true or false)
end
function writer_set:store_links(store)
C.mz_zip_writer_set_store_links(self, store and true or false)
end
function writer_set:zip_cd(zip_it)
C.mz_zip_writer_set_zip_cd(self, zip_it and true or false)
end
function writer:set_cert(path, pwd)
assert(checkok(C.mz_zip_writer_set_certificate(self, path, pwd)))
end
function writer_get:zip_handle()
assert(checkok(C.mz_zip_writer_get_zip_handle(self, vbuf)))
return vbuf[0]
end
metatype('struct minizip_reader_t', gettersandsetters(reader_get, reader_set, reader))
metatype('struct minizip_writer_t', gettersandsetters(writer_get, writer_set, writer))