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