diff options
-rwxr-xr-x | binary/all_in_one | 1 | ||||
-rw-r--r-- | spec/fixtures/abc.bz2 | bin | 0 -> 66 bytes | |||
-rw-r--r-- | spec/fs_spec.lua | 16 | ||||
-rw-r--r-- | src/luarocks/fs/lua.lua | 148 | ||||
-rw-r--r-- | src/luarocks/fs/unix/tools.lua | 60 | ||||
-rw-r--r-- | src/luarocks/fs/win32/tools.lua | 102 | ||||
-rw-r--r-- | src/luarocks/tools/zip.lua | 368 |
7 files changed, 504 insertions, 191 deletions
diff --git a/binary/all_in_one b/binary/all_in_one index 882289f5..dcef0fbf 100755 --- a/binary/all_in_one +++ b/binary/all_in_one | |||
@@ -463,7 +463,6 @@ local function main() | |||
463 | 463 | ||
464 | local dependencies = { | 464 | local dependencies = { |
465 | md5 = "md5", | 465 | md5 = "md5", |
466 | luazip = if_platform("unix", "luazip"), | ||
467 | luasec = "./binary/luasec-0.7alpha-2.rockspec", | 466 | luasec = "./binary/luasec-0.7alpha-2.rockspec", |
468 | luaposix = if_platform("unix", "./binary/luaposix-34.0.4-1.rockspec"), | 467 | luaposix = if_platform("unix", "./binary/luaposix-34.0.4-1.rockspec"), |
469 | luasocket = "luasocket", | 468 | luasocket = "luasocket", |
diff --git a/spec/fixtures/abc.bz2 b/spec/fixtures/abc.bz2 new file mode 100644 index 00000000..ee786715 --- /dev/null +++ b/spec/fixtures/abc.bz2 | |||
Binary files differ | |||
diff --git a/spec/fs_spec.lua b/spec/fs_spec.lua index 66453404..5bec6168 100644 --- a/spec/fs_spec.lua +++ b/spec/fs_spec.lua | |||
@@ -1293,6 +1293,22 @@ describe("Luarocks fs test #unit", function() | |||
1293 | assert.falsy(exists_file("nonexistent")) | 1293 | assert.falsy(exists_file("nonexistent")) |
1294 | end) | 1294 | end) |
1295 | end) | 1295 | end) |
1296 | |||
1297 | describe("fs.bunzip2", function() | ||
1298 | |||
1299 | it("uncompresses a .bz2 file", function() | ||
1300 | local input = testing_paths.fixtures_dir .. "/abc.bz2" | ||
1301 | local output = os.tmpname() | ||
1302 | assert.truthy(fs.bunzip2(input, output)) | ||
1303 | local fd = io.open(output, "r") | ||
1304 | local content = fd:read("*a") | ||
1305 | fd:close() | ||
1306 | assert.same(300000, #content) | ||
1307 | local abc = ("a"):rep(100000)..("b"):rep(100000)..("c"):rep(100000) | ||
1308 | assert.same(abc, content) | ||
1309 | end) | ||
1310 | |||
1311 | end) | ||
1296 | 1312 | ||
1297 | describe("fs.unzip", function() | 1313 | describe("fs.unzip", function() |
1298 | local tmpdir | 1314 | local tmpdir |
diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua index f2fa50b6..375cdee3 100644 --- a/src/luarocks/fs/lua.lua +++ b/src/luarocks/fs/lua.lua | |||
@@ -1,6 +1,6 @@ | |||
1 | 1 | ||
2 | --- Native Lua implementation of filesystem and platform abstractions, | 2 | --- Native Lua implementation of filesystem and platform abstractions, |
3 | -- using LuaFileSystem, LZLib, MD5 and LuaCurl. | 3 | -- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5. |
4 | -- module("luarocks.fs.lua") | 4 | -- module("luarocks.fs.lua") |
5 | local fs_lua = {} | 5 | local fs_lua = {} |
6 | 6 | ||
@@ -10,20 +10,21 @@ local cfg = require("luarocks.core.cfg") | |||
10 | local dir = require("luarocks.dir") | 10 | local dir = require("luarocks.dir") |
11 | local util = require("luarocks.util") | 11 | local util = require("luarocks.util") |
12 | 12 | ||
13 | local socket_ok, zip_ok, unzip_ok, lfs_ok, md5_ok, posix_ok, _ | 13 | local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _ |
14 | local http, ftp, lrzip, luazip, lfs, md5, posix | 14 | local http, ftp, zip, lfs, md5, posix, bz2 |
15 | 15 | ||
16 | if cfg.fs_use_modules then | 16 | if cfg.fs_use_modules then |
17 | socket_ok, http = pcall(require, "socket.http") | 17 | socket_ok, http = pcall(require, "socket.http") |
18 | _, ftp = pcall(require, "socket.ftp") | 18 | _, ftp = pcall(require, "socket.ftp") |
19 | zip_ok, lrzip = pcall(require, "luarocks.tools.zip") | 19 | zip_ok, zip = pcall(require, "luarocks.tools.zip") |
20 | unzip_ok, luazip = pcall(require, "zip"); _G.zip = nil | 20 | bz2_ok, bz2 = pcall(require, "luarocks.tools.bzip2") |
21 | lfs_ok, lfs = pcall(require, "lfs") | 21 | lfs_ok, lfs = pcall(require, "lfs") |
22 | md5_ok, md5 = pcall(require, "md5") | 22 | md5_ok, md5 = pcall(require, "md5") |
23 | posix_ok, posix = pcall(require, "posix") | 23 | posix_ok, posix = pcall(require, "posix") |
24 | end | 24 | end |
25 | 25 | ||
26 | local patch = require("luarocks.tools.patch") | 26 | local patch = require("luarocks.tools.patch") |
27 | local tar = require("luarocks.tools.tar") | ||
27 | 28 | ||
28 | local dir_stack = {} | 29 | local dir_stack = {} |
29 | 30 | ||
@@ -589,52 +590,54 @@ end | |||
589 | end | 590 | end |
590 | 591 | ||
591 | --------------------------------------------------------------------- | 592 | --------------------------------------------------------------------- |
592 | -- LuaZip functions | 593 | -- lua-bz2 functions |
594 | --------------------------------------------------------------------- | ||
595 | |||
596 | if bz2_ok then | ||
597 | |||
598 | local function bunzip2_string(data) | ||
599 | local decompressor = bz2.initDecompress() | ||
600 | local output, err = decompressor:update(data) | ||
601 | if not output then | ||
602 | return nil, err | ||
603 | end | ||
604 | decompressor:close() | ||
605 | return output | ||
606 | end | ||
607 | |||
608 | --- Uncompresses a .bz2 file. | ||
609 | -- @param infile string: pathname of .bz2 file to be extracted. | ||
610 | -- @param outfile string or nil: pathname of output file to be produced. | ||
611 | -- If not given, name is derived from input file. | ||
612 | -- @return boolean: true on success; nil and error message on failure. | ||
613 | function fs_lua.bunzip2(infile, outfile) | ||
614 | assert(type(infile) == "string") | ||
615 | assert(outfile == nil or type(outfile) == "string") | ||
616 | if not outfile then | ||
617 | outfile = infile:gsub("%.bz2$", "") | ||
618 | end | ||
619 | |||
620 | return fs.filter_file(bunzip2_string, infile, outfile) | ||
621 | end | ||
622 | |||
623 | end | ||
624 | |||
625 | --------------------------------------------------------------------- | ||
626 | -- luarocks.tools.zip functions | ||
593 | --------------------------------------------------------------------- | 627 | --------------------------------------------------------------------- |
594 | 628 | ||
595 | if zip_ok then | 629 | if zip_ok then |
596 | 630 | ||
597 | function fs_lua.zip(zipfile, ...) | 631 | function fs_lua.zip(zipfile, ...) |
598 | return lrzip.zip(zipfile, ...) | 632 | return zip.zip(zipfile, ...) |
599 | end | 633 | end |
600 | 634 | ||
635 | function fs_lua.unzip(zipfile) | ||
636 | return zip.unzip(zipfile) | ||
601 | end | 637 | end |
602 | 638 | ||
603 | if unzip_ok then | 639 | function fs_lua.gunzip(infile, outfile) |
604 | --- Uncompress files from a .zip archive. | 640 | return zip.gunzip(infile, outfile) |
605 | -- @param filename string: pathname of .zip archive to be extracted. | ||
606 | -- @return boolean: true on success, false on failure. | ||
607 | function fs_lua.unzip(filename) | ||
608 | local zipfile, err = luazip.open(filename) | ||
609 | if not zipfile then return nil, err end | ||
610 | local files = zipfile:files() | ||
611 | local file = files() | ||
612 | repeat | ||
613 | if file.filename:sub(#file.filename) == "/" then | ||
614 | local ok, err = fs.make_dir(dir.path(fs.current_dir(), file.filename)) | ||
615 | if not ok then return nil, err end | ||
616 | else | ||
617 | local base = dir.dir_name(file.filename) | ||
618 | if base ~= "" then | ||
619 | base = dir.path(fs.current_dir(), base) | ||
620 | if not fs.is_dir(base) then | ||
621 | local ok, err = fs.make_dir(base) | ||
622 | if not ok then return nil, err end | ||
623 | end | ||
624 | end | ||
625 | local rf, err = zipfile:open(file.filename) | ||
626 | if not rf then zipfile:close(); return nil, err end | ||
627 | local contents = rf:read("*a") | ||
628 | rf:close() | ||
629 | local wf, err = io.open(dir.path(fs.current_dir(), file.filename), "wb") | ||
630 | if not wf then zipfile:close(); return nil, err end | ||
631 | wf:write(contents) | ||
632 | wf:close() | ||
633 | end | ||
634 | file = files() | ||
635 | until not file | ||
636 | zipfile:close() | ||
637 | return true | ||
638 | end | 641 | end |
639 | 642 | ||
640 | end | 643 | end |
@@ -878,6 +881,16 @@ local octal_to_rwx = { | |||
878 | ["7"] = "rwx", | 881 | ["7"] = "rwx", |
879 | } | 882 | } |
880 | 883 | ||
884 | function fs_lua._unix_rwx_to_number(rwx) | ||
885 | local num = 0 | ||
886 | for i = 1, 9 do | ||
887 | if rwx:sub(10 - i, 10 - i) == "-" then | ||
888 | num = num + 2^i | ||
889 | end | ||
890 | end | ||
891 | return num | ||
892 | end | ||
893 | |||
881 | do | 894 | do |
882 | local umask_cache | 895 | local umask_cache |
883 | function fs_lua._unix_umask() | 896 | function fs_lua._unix_umask() |
@@ -886,13 +899,8 @@ do | |||
886 | end | 899 | end |
887 | -- LuaPosix (as of 34.0.4) only returns the umask as rwx | 900 | -- LuaPosix (as of 34.0.4) only returns the umask as rwx |
888 | local rwx = posix.umask() | 901 | local rwx = posix.umask() |
889 | local oct = 0 | 902 | local num = fs_lua._unix_rwx_to_number(rwx) |
890 | for i = 1, 9 do | 903 | umask_cache = ("%03o"):format(num) |
891 | if rwx:sub(10 - i, 10 - i) == "-" then | ||
892 | oct = oct + 2^i | ||
893 | end | ||
894 | end | ||
895 | umask_cache = ("%03o"):format(oct) | ||
896 | return umask_cache | 904 | return umask_cache |
897 | end | 905 | end |
898 | end | 906 | end |
@@ -1070,4 +1078,46 @@ function fs_lua.is_lua(filename) | |||
1070 | return (result == true) | 1078 | return (result == true) |
1071 | end | 1079 | end |
1072 | 1080 | ||
1081 | --- Unpack an archive. | ||
1082 | -- Extract the contents of an archive, detecting its format by | ||
1083 | -- filename extension. | ||
1084 | -- @param archive string: Filename of archive. | ||
1085 | -- @return boolean or (boolean, string): true on success, false and an error message on failure. | ||
1086 | function fs_lua.unpack_archive(archive) | ||
1087 | assert(type(archive) == "string") | ||
1088 | |||
1089 | local ok, err | ||
1090 | archive = fs.absolute_name(archive) | ||
1091 | if archive:match("%.tar%.gz$") then | ||
1092 | local tar_filename = archive:gsub("%.gz$", "") | ||
1093 | ok, err = fs.gunzip(archive, tar_filename) | ||
1094 | if ok then | ||
1095 | ok, err = tar.untar(tar_filename, ".") | ||
1096 | end | ||
1097 | elseif archive:match("%.tgz$") then | ||
1098 | local tar_filename = archive:gsub("%.tgz$", ".tar") | ||
1099 | ok, err = fs.gunzip(archive, tar_filename) | ||
1100 | if ok then | ||
1101 | ok, err = tar.untar(tar_filename, ".") | ||
1102 | end | ||
1103 | elseif archive:match("%.tar%.bz2$") then | ||
1104 | local tar_filename = archive:gsub("%.bz2$", "") | ||
1105 | ok, err = fs.bunzip2(archive, tar_filename) | ||
1106 | if ok then | ||
1107 | ok, err = tar.untar(tar_filename, ".") | ||
1108 | end | ||
1109 | elseif archive:match("%.zip$") then | ||
1110 | ok, err = fs.unzip(archive) | ||
1111 | elseif archive:match("%.lua$") or archive:match("%.c$") then | ||
1112 | -- Ignore .lua and .c files; they don't need to be extracted. | ||
1113 | return true | ||
1114 | else | ||
1115 | return false, "Couldn't extract archive "..archive..": unrecognized filename extension" | ||
1116 | end | ||
1117 | if not ok then | ||
1118 | return false, "Failed extracting "..archive..": "..err | ||
1119 | end | ||
1120 | return true | ||
1121 | end | ||
1122 | |||
1073 | return fs_lua | 1123 | return fs_lua |
diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua index de63eb81..d6277119 100644 --- a/src/luarocks/fs/unix/tools.lua +++ b/src/luarocks/fs/unix/tools.lua | |||
@@ -136,6 +136,33 @@ function tools.unzip(zipfile) | |||
136 | return fs.execute_quiet(vars.UNZIP, zipfile) | 136 | return fs.execute_quiet(vars.UNZIP, zipfile) |
137 | end | 137 | end |
138 | 138 | ||
139 | local function uncompress(default_ext, program, infile, outfile) | ||
140 | assert(type(infile) == "string") | ||
141 | assert(outfile == nil or type(outfile) == "string") | ||
142 | if not outfile then | ||
143 | outfile = infile:gsub("%."..default_ext.."$", "") | ||
144 | end | ||
145 | return fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) | ||
146 | end | ||
147 | |||
148 | --- Uncompresses a .gz file. | ||
149 | -- @param infile string: pathname of .gz file to be extracted. | ||
150 | -- @param outfile string or nil: pathname of output file to be produced. | ||
151 | -- If not given, name is derived from input file. | ||
152 | -- @return boolean: true on success; nil and error message on failure. | ||
153 | function tools.gunzip(infile, outfile) | ||
154 | return uncompress("gz", "gunzip", infile, outfile) | ||
155 | end | ||
156 | |||
157 | --- Uncompresses a .bz2 file. | ||
158 | -- @param infile string: pathname of .bz2 file to be extracted. | ||
159 | -- @param outfile string or nil: pathname of output file to be produced. | ||
160 | -- If not given, name is derived from input file. | ||
161 | -- @return boolean: true on success; nil and error message on failure. | ||
162 | function tools.bunzip2(infile, outfile) | ||
163 | return uncompress("bz2", "bunzip2", infile, outfile) | ||
164 | end | ||
165 | |||
139 | --- Test is file/directory exists | 166 | --- Test is file/directory exists |
140 | -- @param file string: filename to test | 167 | -- @param file string: filename to test |
141 | -- @return boolean: true if file exists, false otherwise. | 168 | -- @return boolean: true if file exists, false otherwise. |
@@ -198,39 +225,6 @@ function tools.set_permissions(filename, mode, scope) | |||
198 | return fs.execute(vars.CHMOD, perms, filename) | 225 | return fs.execute(vars.CHMOD, perms, filename) |
199 | end | 226 | end |
200 | 227 | ||
201 | --- Unpack an archive. | ||
202 | -- Extract the contents of an archive, detecting its format by | ||
203 | -- filename extension. | ||
204 | -- @param archive string: Filename of archive. | ||
205 | -- @return boolean or (boolean, string): true on success, false and an error message on failure. | ||
206 | function tools.unpack_archive(archive) | ||
207 | assert(type(archive) == "string") | ||
208 | |||
209 | local pipe_to_tar = " | "..vars.TAR.." -xf -" | ||
210 | |||
211 | if not cfg.verbose then | ||
212 | pipe_to_tar = " 2> /dev/null"..fs.quiet(pipe_to_tar) | ||
213 | end | ||
214 | |||
215 | local ok | ||
216 | if archive:match("%.tar%.gz$") or archive:match("%.tgz$") then | ||
217 | ok = fs.execute_string(vars.GUNZIP.." -c "..fs.Q(archive)..pipe_to_tar) | ||
218 | elseif archive:match("%.tar%.bz2$") then | ||
219 | ok = fs.execute_string(vars.BUNZIP2.." -c "..fs.Q(archive)..pipe_to_tar) | ||
220 | elseif archive:match("%.zip$") then | ||
221 | ok = fs.execute_quiet(vars.UNZIP, archive) | ||
222 | elseif archive:match("%.lua$") or archive:match("%.c$") then | ||
223 | -- Ignore .lua and .c files; they don't need to be extracted. | ||
224 | return true | ||
225 | else | ||
226 | return false, "Couldn't extract archive "..archive..": unrecognized filename extension" | ||
227 | end | ||
228 | if not ok then | ||
229 | return false, "Failed extracting "..archive | ||
230 | end | ||
231 | return true | ||
232 | end | ||
233 | |||
234 | function tools.attributes(filename, attrtype) | 228 | function tools.attributes(filename, attrtype) |
235 | local flag = ((attrtype == "permissions") and vars.STATPERMFLAG) | 229 | local flag = ((attrtype == "permissions") and vars.STATPERMFLAG) |
236 | or ((attrtype == "owner") and vars.STATOWNERFLAG) | 230 | or ((attrtype == "owner") and vars.STATOWNERFLAG) |
diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua index 0a27cbe3..633aae5e 100644 --- a/src/luarocks/fs/win32/tools.lua +++ b/src/luarocks/fs/win32/tools.lua | |||
@@ -148,6 +148,51 @@ function tools.unzip(zipfile) | |||
148 | return fs.execute_quiet(fs.Q(vars.SEVENZ).." -aoa x", zipfile) | 148 | return fs.execute_quiet(fs.Q(vars.SEVENZ).." -aoa x", zipfile) |
149 | end | 149 | end |
150 | 150 | ||
151 | local function sevenz(default_ext, infile, outfile) | ||
152 | assert(type(infile) == "string") | ||
153 | assert(outfile == nil or type(outfile) == "string") | ||
154 | |||
155 | local dropext = infile:gsub("%."..default_ext.."$", "") | ||
156 | local outdir = dir.dir_name(dropext) | ||
157 | |||
158 | infile = fs.absolute_name(infile) | ||
159 | |||
160 | local cmdline = fs.Q(vars.SEVENZ).." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile) | ||
161 | local ok, err = fs.execute_quiet(cmdline) | ||
162 | if not ok then | ||
163 | return nil, err | ||
164 | end | ||
165 | |||
166 | if outfile then | ||
167 | outfile = fs.absolute_name(outfile) | ||
168 | dropext = fs.absolute_name(dropext) | ||
169 | ok, err = os.rename(dropext, outfile) | ||
170 | if not ok then | ||
171 | return nil, err | ||
172 | end | ||
173 | end | ||
174 | |||
175 | return true | ||
176 | end | ||
177 | |||
178 | --- Uncompresses a .gz file. | ||
179 | -- @param infile string: pathname of .gz file to be extracted. | ||
180 | -- @param outfile string or nil: pathname of output file to be produced. | ||
181 | -- If not given, name is derived from input file. | ||
182 | -- @return boolean: true on success; nil and error message on failure. | ||
183 | function tools.gunzip(infile, outfile) | ||
184 | return sevenz("gz", infile, outfile) | ||
185 | end | ||
186 | |||
187 | --- Uncompresses a .bz2 file. | ||
188 | -- @param infile string: pathname of .bz2 file to be extracted. | ||
189 | -- @param outfile string or nil: pathname of output file to be produced. | ||
190 | -- If not given, name is derived from input file. | ||
191 | -- @return boolean: true on success; nil and error message on failure. | ||
192 | function tools.bunzip2(infile, outfile) | ||
193 | return sevenz("bz2", infile, outfile) | ||
194 | end | ||
195 | |||
151 | --- Test is pathname is a directory. | 196 | --- Test is pathname is a directory. |
152 | -- @param file string: pathname to test | 197 | -- @param file string: pathname to test |
153 | -- @return boolean: true if it is a directory, false otherwise. | 198 | -- @return boolean: true if it is a directory, false otherwise. |
@@ -241,63 +286,6 @@ function tools.set_permissions(filename, mode, scope) | |||
241 | return true | 286 | return true |
242 | end | 287 | end |
243 | 288 | ||
244 | |||
245 | --- Strip the last extension of a filename. | ||
246 | -- Example: "foo.tar.gz" becomes "foo.tar". | ||
247 | -- If filename has no dots, returns it unchanged. | ||
248 | -- @param filename string: The file name to strip. | ||
249 | -- @return string: The stripped name. | ||
250 | local function strip_extension(filename) | ||
251 | assert(type(filename) == "string") | ||
252 | return (filename:gsub("%.[^.]+$", "")) or filename | ||
253 | end | ||
254 | |||
255 | --- Uncompress gzip file. | ||
256 | -- @param archive string: Filename of archive. | ||
257 | -- @return boolean : success status | ||
258 | local function gunzip(archive) | ||
259 | return fs.execute_quiet(fs.Q(vars.SEVENZ).." -aoa x", archive) | ||
260 | end | ||
261 | |||
262 | --- Unpack an archive. | ||
263 | -- Extract the contents of an archive, detecting its format by | ||
264 | -- filename extension. | ||
265 | -- @param archive string: Filename of archive. | ||
266 | -- @return boolean or (boolean, string): true on success, false and an error message on failure. | ||
267 | function tools.unpack_archive(archive) | ||
268 | assert(type(archive) == "string") | ||
269 | |||
270 | local ok | ||
271 | local sevenzx = fs.Q(vars.SEVENZ).." -aoa x" | ||
272 | if archive:match("%.tar%.gz$") then | ||
273 | ok = gunzip(archive) | ||
274 | if ok then | ||
275 | ok = fs.execute_quiet(sevenzx, strip_extension(archive)) | ||
276 | end | ||
277 | elseif archive:match("%.tgz$") then | ||
278 | ok = gunzip(archive) | ||
279 | if ok then | ||
280 | ok = fs.execute_quiet(sevenzx, strip_extension(archive)..".tar") | ||
281 | end | ||
282 | elseif archive:match("%.tar%.bz2$") then | ||
283 | ok = fs.execute_quiet(sevenzx, archive) | ||
284 | if ok then | ||
285 | ok = fs.execute_quiet(sevenzx, strip_extension(archive)) | ||
286 | end | ||
287 | elseif archive:match("%.zip$") then | ||
288 | ok = fs.execute_quiet(sevenzx, archive) | ||
289 | elseif archive:match("%.lua$") or archive:match("%.c$") then | ||
290 | -- Ignore .lua and .c files; they don't need to be extracted. | ||
291 | return true | ||
292 | else | ||
293 | return false, "Couldn't extract archive "..archive..": unrecognized filename extension" | ||
294 | end | ||
295 | if not ok then | ||
296 | return false, "Failed extracting "..archive | ||
297 | end | ||
298 | return true | ||
299 | end | ||
300 | |||
301 | --- Test for existance of a file. | 289 | --- Test for existance of a file. |
302 | -- @param file string: filename to test | 290 | -- @param file string: filename to test |
303 | -- @return boolean: true if file exists, false otherwise. | 291 | -- @return boolean: true if file exists, false otherwise. |
diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua index e6d9e36a..5974c7bf 100644 --- a/src/luarocks/tools/zip.lua +++ b/src/luarocks/tools/zip.lua | |||
@@ -1,26 +1,58 @@ | |||
1 | 1 | ||
2 | --- A Lua implementation of .zip file archiving (used for creating .rock files), | 2 | --- A Lua implementation of .zip and .gz file compression and decompression, |
3 | -- using only lzlib or lua-lzib. | 3 | -- using only lzlib or lua-lzib. |
4 | local zip = {} | 4 | local zip = {} |
5 | 5 | ||
6 | local zlib = require("zlib") | 6 | local zlib = require("zlib") |
7 | local fs = require("luarocks.fs") | 7 | local fs = require("luarocks.fs") |
8 | local fun = require("luarocks.fun") | ||
8 | local dir = require("luarocks.dir") | 9 | local dir = require("luarocks.dir") |
9 | 10 | ||
11 | local stat_ok, stat = pcall(require, "posix.sys.stat") | ||
12 | |||
13 | local function shr(n, m) | ||
14 | return math.floor(n / 2^m) | ||
15 | end | ||
16 | |||
17 | local function shl(n, m) | ||
18 | return n * 2^m | ||
19 | end | ||
20 | local function lowbits(n, m) | ||
21 | return n % 2^m | ||
22 | end | ||
23 | |||
24 | local 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 | ||
32 | end | ||
33 | |||
10 | -- zlib module can be provided by both lzlib and lua-lzib packages. | 34 | -- zlib module can be provided by both lzlib and lua-lzib packages. |
11 | -- Create a compatibility layer. | 35 | -- Create a compatibility layer. |
12 | local zlib_compress, zlib_crc32 | 36 | local zlib_compress, zlib_uncompress, zlib_crc32 |
13 | if zlib._VERSION:match "^lua%-zlib" then | 37 | if zlib._VERSION:match "^lua%-zlib" then |
14 | function zlib_compress(data) | 38 | function zlib_compress(data, mode) |
15 | return (zlib.deflate()(data, "finish")) | 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)) | ||
16 | end | 44 | end |
17 | 45 | ||
18 | function zlib_crc32(data) | 46 | function zlib_crc32(data) |
19 | return zlib.crc32()(data) | 47 | return zlib.crc32()(data) |
20 | end | 48 | end |
21 | elseif zlib._VERSION:match "^lzlib" then | 49 | elseif zlib._VERSION:match "^lzlib" then |
22 | function zlib_compress(data) | 50 | function zlib_compress(data, mode) |
23 | return zlib.compress(data) | 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)) | ||
24 | end | 56 | end |
25 | 57 | ||
26 | function zlib_crc32(data) | 58 | function zlib_crc32(data) |
@@ -30,7 +62,7 @@ else | |||
30 | error("unknown zlib library", 0) | 62 | error("unknown zlib library", 0) |
31 | end | 63 | end |
32 | 64 | ||
33 | local function number_to_bytestring(number, nbytes) | 65 | local function number_to_lestring(number, nbytes) |
34 | local out = {} | 66 | local out = {} |
35 | for _ = 1, nbytes do | 67 | for _ = 1, nbytes do |
36 | local byte = number % 256 | 68 | local byte = number % 256 |
@@ -40,6 +72,20 @@ local function number_to_bytestring(number, nbytes) | |||
40 | return table.concat(out) | 72 | return table.concat(out) |
41 | end | 73 | end |
42 | 74 | ||
75 | local 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) | ||
82 | end | ||
83 | |||
84 | local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4) | ||
85 | local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4) | ||
86 | local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4) | ||
87 | local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4) | ||
88 | |||
43 | --- Begin a new file to be stored inside the zipfile. | 89 | --- Begin a new file to be stored inside the zipfile. |
44 | -- @param self handle of the zipfile being written. | 90 | -- @param self handle of the zipfile being written. |
45 | -- @param filename filenome of the file to be added to the zipfile. | 91 | -- @param filename filenome of the file to be added to the zipfile. |
@@ -56,7 +102,7 @@ local function zipwriter_open_new_file_in_zip(self, filename) | |||
56 | lfh.file_name_length = #filename | 102 | lfh.file_name_length = #filename |
57 | lfh.extra_field_length = 0 | 103 | lfh.extra_field_length = 0 |
58 | lfh.file_name = filename:gsub("\\", "/") | 104 | lfh.file_name = filename:gsub("\\", "/") |
59 | lfh.external_attr = 0 -- TODO properly store permissions | 105 | lfh.external_attr = shl(493, 16) -- TODO proper permissions |
60 | self.in_open_file = true | 106 | self.in_open_file = true |
61 | return true | 107 | return true |
62 | end | 108 | end |
@@ -70,7 +116,7 @@ local function zipwriter_write_file_in_zip(self, data) | |||
70 | return nil | 116 | return nil |
71 | end | 117 | end |
72 | local lfh = self.local_file_header | 118 | local lfh = self.local_file_header |
73 | local compressed = zlib_compress(data):sub(3, -5) | 119 | local compressed = zlib_compress(data, "raw") |
74 | lfh.crc32 = zlib_crc32(data) | 120 | lfh.crc32 = zlib_crc32(data) |
75 | lfh.compressed_size = #compressed | 121 | lfh.compressed_size = #compressed |
76 | lfh.uncompressed_size = #data | 122 | lfh.uncompressed_size = #data |
@@ -91,26 +137,27 @@ local function zipwriter_close_file_in_zip(self) | |||
91 | -- Local file header | 137 | -- Local file header |
92 | local lfh = self.local_file_header | 138 | local lfh = self.local_file_header |
93 | lfh.offset = zh:seek() | 139 | lfh.offset = zh:seek() |
94 | zh:write(number_to_bytestring(0x04034b50, 4)) -- signature | 140 | zh:write(LOCAL_FILE_HEADER_SIGNATURE) |
95 | zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 | 141 | zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 |
96 | zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag | 142 | zh:write(number_to_lestring(4, 2)) -- general purpose bit flag |
97 | zh:write(number_to_bytestring(8, 2)) -- compression method: deflate | 143 | zh:write(number_to_lestring(8, 2)) -- compression method: deflate |
98 | zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) | 144 | zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) |
99 | zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) | 145 | zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) |
100 | zh:write(number_to_bytestring(lfh.crc32, 4)) | 146 | zh:write(number_to_lestring(lfh.crc32, 4)) |
101 | zh:write(number_to_bytestring(lfh.compressed_size, 4)) | 147 | zh:write(number_to_lestring(lfh.compressed_size, 4)) |
102 | zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) | 148 | zh:write(number_to_lestring(lfh.uncompressed_size, 4)) |
103 | zh:write(number_to_bytestring(lfh.file_name_length, 2)) | 149 | zh:write(number_to_lestring(lfh.file_name_length, 2)) |
104 | zh:write(number_to_bytestring(lfh.extra_field_length, 2)) | 150 | zh:write(number_to_lestring(lfh.extra_field_length, 2)) |
105 | zh:write(lfh.file_name) | 151 | zh:write(lfh.file_name) |
106 | 152 | ||
107 | -- File data | 153 | -- File data |
108 | zh:write(self.data) | 154 | zh:write(self.data) |
109 | 155 | ||
110 | -- Data descriptor | 156 | -- Data descriptor |
111 | zh:write(number_to_bytestring(lfh.crc32, 4)) | 157 | zh:write(DATA_DESCRIPTOR_SIGNATURE) |
112 | zh:write(number_to_bytestring(lfh.compressed_size, 4)) | 158 | zh:write(number_to_lestring(lfh.crc32, 4)) |
113 | zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) | 159 | zh:write(number_to_lestring(lfh.compressed_size, 4)) |
160 | zh:write(number_to_lestring(lfh.uncompressed_size, 4)) | ||
114 | 161 | ||
115 | table.insert(self.files, lfh) | 162 | table.insert(self.files, lfh) |
116 | self.in_open_file = false | 163 | self.in_open_file = false |
@@ -167,36 +214,36 @@ local function zipwriter_close(self) | |||
167 | local size_of_central_directory = 0 | 214 | local size_of_central_directory = 0 |
168 | -- Central directory structure | 215 | -- Central directory structure |
169 | for _, lfh in ipairs(self.files) do | 216 | for _, lfh in ipairs(self.files) do |
170 | zh:write(number_to_bytestring(0x02014b50, 4)) -- signature | 217 | zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature |
171 | zh:write(number_to_bytestring(3, 2)) -- version made by: UNIX | 218 | zh:write(number_to_lestring(3, 2)) -- version made by: UNIX |
172 | zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 | 219 | zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 |
173 | zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag | 220 | zh:write(number_to_lestring(0, 2)) -- general purpose bit flag |
174 | zh:write(number_to_bytestring(8, 2)) -- compression method: deflate | 221 | zh:write(number_to_lestring(8, 2)) -- compression method: deflate |
175 | zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) | 222 | zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) |
176 | zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) | 223 | zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) |
177 | zh:write(number_to_bytestring(lfh.crc32, 4)) | 224 | zh:write(number_to_lestring(lfh.crc32, 4)) |
178 | zh:write(number_to_bytestring(lfh.compressed_size, 4)) | 225 | zh:write(number_to_lestring(lfh.compressed_size, 4)) |
179 | zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) | 226 | zh:write(number_to_lestring(lfh.uncompressed_size, 4)) |
180 | zh:write(number_to_bytestring(lfh.file_name_length, 2)) | 227 | zh:write(number_to_lestring(lfh.file_name_length, 2)) |
181 | zh:write(number_to_bytestring(lfh.extra_field_length, 2)) | 228 | zh:write(number_to_lestring(lfh.extra_field_length, 2)) |
182 | zh:write(number_to_bytestring(0, 2)) -- file comment length | 229 | zh:write(number_to_lestring(0, 2)) -- file comment length |
183 | zh:write(number_to_bytestring(0, 2)) -- disk number start | 230 | zh:write(number_to_lestring(0, 2)) -- disk number start |
184 | zh:write(number_to_bytestring(0, 2)) -- internal file attributes | 231 | zh:write(number_to_lestring(0, 2)) -- internal file attributes |
185 | zh:write(number_to_bytestring(lfh.external_attr, 4)) -- external file attributes | 232 | zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes |
186 | zh:write(number_to_bytestring(lfh.offset, 4)) -- relative offset of local header | 233 | zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header |
187 | zh:write(lfh.file_name) | 234 | zh:write(lfh.file_name) |
188 | size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length | 235 | size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length |
189 | end | 236 | end |
190 | 237 | ||
191 | -- End of central directory record | 238 | -- End of central directory record |
192 | zh:write(number_to_bytestring(0x06054b50, 4)) -- signature | 239 | zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature |
193 | zh:write(number_to_bytestring(0, 2)) -- number of this disk | 240 | zh:write(number_to_lestring(0, 2)) -- number of this disk |
194 | zh:write(number_to_bytestring(0, 2)) -- number of disk with start of central directory | 241 | zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory |
195 | zh:write(number_to_bytestring(#self.files, 2)) -- total number of entries in the central dir on this disk | 242 | zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk |
196 | zh:write(number_to_bytestring(#self.files, 2)) -- total number of entries in the central dir | 243 | zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir |
197 | zh:write(number_to_bytestring(size_of_central_directory, 4)) | 244 | zh:write(number_to_lestring(size_of_central_directory, 4)) |
198 | zh:write(number_to_bytestring(central_directory_offset, 4)) | 245 | zh:write(number_to_lestring(central_directory_offset, 4)) |
199 | zh:write(number_to_bytestring(0, 2)) -- zip file comment length | 246 | zh:write(number_to_lestring(0, 2)) -- zip file comment length |
200 | zh:close() | 247 | zh:close() |
201 | 248 | ||
202 | return true | 249 | return true |
@@ -253,12 +300,231 @@ function zip.zip(zipfile, ...) | |||
253 | end | 300 | end |
254 | end | 301 | end |
255 | 302 | ||
256 | ok = zw:close() | 303 | zw:close() |
304 | return ok, err | ||
305 | end | ||
306 | |||
307 | |||
308 | local function ziptime_to_luatime(ztime, zdate) | ||
309 | return { | ||
310 | year = shr(zdate, 9) + 1980, | ||
311 | month = shr(lowbits(zdate, 9), 5), | ||
312 | day = lowbits(zdate, 5), | ||
313 | hour = shr(ztime, 11), | ||
314 | min = shr(lowbits(ztime, 11), 5), | ||
315 | sec = lowbits(ztime, 5) * 2, | ||
316 | } | ||
317 | end | ||
318 | |||
319 | local function read_file_in_zip(zh, cdr) | ||
320 | local sig = zh:read(4) | ||
321 | if sig ~= LOCAL_FILE_HEADER_SIGNATURE then | ||
322 | return nil, "failed reading Local File Header signature" | ||
323 | end | ||
324 | |||
325 | local lfh = {} | ||
326 | lfh.version_needed = lestring_to_number(zh:read(2)) | ||
327 | lfh.bitflag = lestring_to_number(zh:read(2)) | ||
328 | lfh.compression_method = lestring_to_number(zh:read(2)) | ||
329 | lfh.last_mod_file_time = lestring_to_number(zh:read(2)) | ||
330 | lfh.last_mod_file_date = lestring_to_number(zh:read(2)) | ||
331 | lfh.crc32 = lestring_to_number(zh:read(4)) | ||
332 | lfh.compressed_size = lestring_to_number(zh:read(4)) | ||
333 | lfh.uncompressed_size = lestring_to_number(zh:read(4)) | ||
334 | lfh.file_name_length = lestring_to_number(zh:read(2)) | ||
335 | lfh.extra_field_length = lestring_to_number(zh:read(2)) | ||
336 | lfh.file_name = zh:read(lfh.file_name_length) | ||
337 | lfh.extra_field = zh:read(lfh.extra_field_length) | ||
338 | |||
339 | local data = zh:read(cdr.compressed_size) | ||
340 | |||
341 | local uncompressed | ||
342 | if cdr.compression_method == 8 then | ||
343 | uncompressed = zlib_uncompress(data, "raw") | ||
344 | elseif cdr.compression_method == 0 then | ||
345 | uncompressed = data | ||
346 | else | ||
347 | return nil, "unknown compression method " .. cdr.compression_method | ||
348 | end | ||
349 | |||
350 | if #uncompressed ~= cdr.uncompressed_size then | ||
351 | return nil, "uncompressed size doesn't match" | ||
352 | end | ||
353 | if cdr.crc32 ~= zlib_crc32(uncompressed) then | ||
354 | return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed | ||
355 | end | ||
356 | |||
357 | return uncompressed | ||
358 | end | ||
359 | |||
360 | local function process_end_of_central_dir(zh) | ||
361 | local at, err = zh:seek("end", -22) | ||
362 | if not at then | ||
363 | return nil, err | ||
364 | end | ||
365 | |||
366 | while true do | ||
367 | local sig = zh:read(4) | ||
368 | if sig == END_OF_CENTRAL_DIR_SIGNATURE then | ||
369 | break | ||
370 | end | ||
371 | at = at - 1 | ||
372 | local at1, err = zh:seek("set", at) | ||
373 | if at1 ~= at then | ||
374 | return nil, "Could not find End of Central Directory signature" | ||
375 | end | ||
376 | end | ||
377 | |||
378 | -- number of this disk (2 bytes) | ||
379 | -- number of the disk with the start of the central directory (2 bytes) | ||
380 | -- total number of entries in the central directory on this disk (2 bytes) | ||
381 | -- total number of entries in the central directory (2 bytes) | ||
382 | zh:seek("cur", 6) | ||
383 | |||
384 | local central_directory_entries = lestring_to_number(zh:read(2)) | ||
385 | |||
386 | -- central directory size (4 bytes) | ||
387 | zh:seek("cur", 4) | ||
388 | |||
389 | local central_directory_offset = lestring_to_number(zh:read(4)) | ||
390 | |||
391 | return central_directory_entries, central_directory_offset | ||
392 | end | ||
393 | |||
394 | local function process_central_dir(zh, cd_entries) | ||
395 | |||
396 | local files = {} | ||
397 | |||
398 | for i = 1, cd_entries do | ||
399 | local sig = zh:read(4) | ||
400 | if sig ~= CENTRAL_DIRECTORY_SIGNATURE then | ||
401 | return nil, "failed reading Central Directory signature" | ||
402 | end | ||
403 | |||
404 | local cdr = {} | ||
405 | files[i] = cdr | ||
406 | |||
407 | cdr.version_made_by = lestring_to_number(zh:read(2)) | ||
408 | cdr.version_needed = lestring_to_number(zh:read(2)) | ||
409 | cdr.bitflag = lestring_to_number(zh:read(2)) | ||
410 | cdr.compression_method = lestring_to_number(zh:read(2)) | ||
411 | cdr.last_mod_file_time = lestring_to_number(zh:read(2)) | ||
412 | cdr.last_mod_file_date = lestring_to_number(zh:read(2)) | ||
413 | cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date) | ||
414 | cdr.crc32 = lestring_to_number(zh:read(4)) | ||
415 | cdr.compressed_size = lestring_to_number(zh:read(4)) | ||
416 | cdr.uncompressed_size = lestring_to_number(zh:read(4)) | ||
417 | cdr.file_name_length = lestring_to_number(zh:read(2)) | ||
418 | cdr.extra_field_length = lestring_to_number(zh:read(2)) | ||
419 | cdr.file_comment_length = lestring_to_number(zh:read(2)) | ||
420 | cdr.disk_number_start = lestring_to_number(zh:read(2)) | ||
421 | cdr.internal_attr = lestring_to_number(zh:read(2)) | ||
422 | cdr.external_attr = lestring_to_number(zh:read(4)) | ||
423 | cdr.offset = lestring_to_number(zh:read(4)) | ||
424 | cdr.file_name = zh:read(cdr.file_name_length) | ||
425 | cdr.extra_field = zh:read(cdr.extra_field_length) | ||
426 | cdr.file_comment = zh:read(cdr.file_comment_length) | ||
427 | end | ||
428 | return files | ||
429 | end | ||
430 | |||
431 | --- Uncompress files from a .zip archive. | ||
432 | -- @param zipfile string: pathname of .zip archive to be created. | ||
433 | -- @return boolean or (boolean, string): true on success, | ||
434 | -- false and an error message on failure. | ||
435 | function zip.unzip(zipfile) | ||
436 | zipfile = fs.absolute_name(zipfile) | ||
437 | local zh, err = io.open(zipfile, "rb") | ||
438 | if not zh then | ||
439 | return nil, err | ||
440 | end | ||
441 | |||
442 | local cd_entries, cd_offset = process_end_of_central_dir(zh) | ||
443 | if not cd_entries then | ||
444 | return nil, cd_offset | ||
445 | end | ||
446 | |||
447 | local ok, err = zh:seek("set", cd_offset) | ||
257 | if not ok then | 448 | if not ok then |
258 | return false, "error closing "..zipfile | 449 | return nil, err |
259 | end | 450 | end |
260 | return ok, err | 451 | |
452 | local files, err = process_central_dir(zh, cd_entries) | ||
453 | if not files then | ||
454 | return nil, err | ||
455 | end | ||
456 | |||
457 | for _, cdr in ipairs(files) do | ||
458 | local file = cdr.file_name | ||
459 | if file:sub(#file) == "/" then | ||
460 | local ok, err = fs.make_dir(dir.path(fs.current_dir(), file)) | ||
461 | if not ok then | ||
462 | return nil, err | ||
463 | end | ||
464 | else | ||
465 | local base = dir.dir_name(file) | ||
466 | if base ~= "" then | ||
467 | base = dir.path(fs.current_dir(), base) | ||
468 | if not fs.is_dir(base) then | ||
469 | local ok, err = fs.make_dir(base) | ||
470 | if not ok then | ||
471 | return nil, err | ||
472 | end | ||
473 | end | ||
474 | end | ||
475 | |||
476 | local ok, err = zh:seek("set", cdr.offset) | ||
477 | if not ok then | ||
478 | return nil, err | ||
479 | end | ||
480 | |||
481 | local contents, err = read_file_in_zip(zh, cdr) | ||
482 | if not contents then | ||
483 | return nil, err | ||
484 | end | ||
485 | local pathname = dir.path(fs.current_dir(), file) | ||
486 | local wf, err = io.open(pathname, "wb") | ||
487 | if not wf then | ||
488 | zh:close() | ||
489 | return nil, err | ||
490 | end | ||
491 | wf:write(contents) | ||
492 | wf:close() | ||
493 | |||
494 | if cdr.external_attr > 0 then | ||
495 | fs.set_permissions(pathname, "exec", "all") | ||
496 | else | ||
497 | fs.set_permissions(pathname, "read", "all") | ||
498 | end | ||
499 | fs.set_time(pathname, cdr.last_mod_luatime) | ||
500 | end | ||
501 | end | ||
502 | zh:close() | ||
503 | return true | ||
261 | end | 504 | end |
262 | 505 | ||
506 | function zip.gzip(input_filename, output_filename) | ||
507 | assert(type(input_filename) == "string") | ||
508 | assert(output_filename == nil or type(output_filename) == "string") | ||
509 | |||
510 | if not output_filename then | ||
511 | output_filename = input_filename .. ".gz" | ||
512 | end | ||
513 | |||
514 | local fn = fun.partial(fun.flip(zlib_compress), "gzip") | ||
515 | return fs.filter_file(fn, input_filename, output_filename) | ||
516 | end | ||
517 | |||
518 | function zip.gunzip(input_filename, output_filename) | ||
519 | assert(type(input_filename) == "string") | ||
520 | assert(output_filename == nil or type(output_filename) == "string") | ||
521 | |||
522 | if not output_filename then | ||
523 | output_filename = input_filename:gsub("%.gz$", "") | ||
524 | end | ||
525 | |||
526 | local fn = fun.partial(fun.flip(zlib_uncompress), "gzip") | ||
527 | return fs.filter_file(fn, input_filename, output_filename) | ||
528 | end | ||
263 | 529 | ||
264 | return zip | 530 | return zip |