summaryrefslogtreecommitdiff
path: root/src/luarocks/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks/tools')
-rw-r--r--src/luarocks/tools/patch.lua716
-rw-r--r--src/luarocks/tools/tar.lua191
-rw-r--r--src/luarocks/tools/zip.lua531
3 files changed, 1438 insertions, 0 deletions
diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua
new file mode 100644
index 0000000..6f36d71
--- /dev/null
+++ b/src/luarocks/tools/patch.lua
@@ -0,0 +1,716 @@
+--- Patch utility to apply unified diffs.
+--
+-- http://lua-users.org/wiki/LuaPatch
+--
+-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license).
+-- Code is heavily based on the Python-based patch.py version 8.06-1
+-- Copyright (c) 2008 rainforce.org, MIT License
+-- Project home: http://code.google.com/p/python-patch/ .
+-- Version 0.1
+
+local patch = {}
+
+local fs = require("luarocks.fs")
+local fun = require("luarocks.fun")
+
+local io = io
+local os = os
+local string = string
+local table = table
+local format = string.format
+
+-- logging
+local debugmode = false
+local function debug(_) end
+local function info(_) end
+local function warning(s) io.stderr:write(s .. '\n') end
+
+-- Returns boolean whether string s2 starts with string s.
+local function startswith(s, s2)
+ return s:sub(1, #s2) == s2
+end
+
+-- Returns boolean whether string s2 ends with string s.
+local function endswith(s, s2)
+ return #s >= #s2 and s:sub(#s-#s2+1) == s2
+end
+
+-- Returns string s after filtering out any new-line characters from end.
+local function endlstrip(s)
+ return s:gsub('[\r\n]+$', '')
+end
+
+-- Returns shallow copy of table t.
+local function table_copy(t)
+ local t2 = {}
+ for k,v in pairs(t) do t2[k] = v end
+ return t2
+end
+
+local function exists(filename)
+ local fh = io.open(filename)
+ local result = fh ~= nil
+ if fh then fh:close() end
+ return result
+end
+local function isfile() return true end --FIX?
+
+local function string_as_file(s)
+ return {
+ at = 0,
+ str = s,
+ len = #s,
+ eof = false,
+ read = function(self, n)
+ if self.eof then return nil end
+ local chunk = self.str:sub(self.at, self.at + n - 1)
+ self.at = self.at + n
+ if self.at > self.len then
+ self.eof = true
+ end
+ return chunk
+ end,
+ close = function(self)
+ self.eof = true
+ end,
+ }
+end
+
+--
+-- file_lines(f) is similar to f:lines() for file f.
+-- The main difference is that read_lines includes
+-- new-line character sequences ("\n", "\r\n", "\r"),
+-- if any, at the end of each line. Embedded "\0" are also handled.
+-- Caution: The newline behavior can depend on whether f is opened
+-- in binary or ASCII mode.
+-- (file_lines - version 20080913)
+--
+local function file_lines(f)
+ local CHUNK_SIZE = 1024
+ local buffer = ""
+ local pos_beg = 1
+ return function()
+ local pos, chars
+ while 1 do
+ pos, chars = buffer:match('()([\r\n].)', pos_beg)
+ if pos or not f then
+ break
+ elseif f then
+ local chunk = f:read(CHUNK_SIZE)
+ if chunk then
+ buffer = buffer:sub(pos_beg) .. chunk
+ pos_beg = 1
+ else
+ f = nil
+ end
+ end
+ end
+ if not pos then
+ pos = #buffer
+ elseif chars == '\r\n' then
+ pos = pos + 1
+ end
+ local line = buffer:sub(pos_beg, pos)
+ pos_beg = pos + 1
+ if #line > 0 then
+ return line
+ end
+ end
+end
+
+local function match_linerange(line)
+ local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)")
+ if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end
+ if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end
+ if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end
+ return m1, m2, m3, m4
+end
+
+local function match_epoch(str)
+ return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]")
+end
+
+function patch.read_patch(filename, data)
+ -- define possible file regions that will direct the parser flow
+ local state = 'header'
+ -- 'header' - comments before the patch body
+ -- 'filenames' - lines starting with --- and +++
+ -- 'hunkhead' - @@ -R +R @@ sequence
+ -- 'hunkbody'
+ -- 'hunkskip' - skipping invalid hunk mode
+
+ local all_ok = true
+ local lineends = {lf=0, crlf=0, cr=0}
+ local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}}
+ local nextfileno = 0
+ local nexthunkno = 0 --: even if index starts with 0 user messages
+ -- number hunks from 1
+
+ -- hunkinfo holds parsed values, hunkactual - calculated
+ local hunkinfo = {
+ startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil,
+ invalid=false, text={}
+ }
+ local hunkactual = {linessrc=nil, linestgt=nil}
+
+ info(format("reading patch %s", filename))
+
+ local fp
+ if data then
+ fp = string_as_file(data)
+ else
+ fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
+ end
+ local lineno = 0
+
+ for line in file_lines(fp) do
+ lineno = lineno + 1
+ if state == 'header' then
+ if startswith(line, "--- ") then
+ state = 'filenames'
+ end
+ -- state is 'header' or 'filenames'
+ end
+ if state == 'hunkbody' then
+ -- skip hunkskip and hunkbody code until definition of hunkhead read
+
+ if line:match"^[\r\n]*$" then
+ -- prepend space to empty lines to interpret them as context properly
+ line = " " .. line
+ end
+
+ -- process line first
+ if line:match"^[- +\\]" then
+ -- gather stats about line endings
+ local he = files.hunkends[nextfileno]
+ if endswith(line, "\r\n") then
+ he.crlf = he.crlf + 1
+ elseif endswith(line, "\n") then
+ he.lf = he.lf + 1
+ elseif endswith(line, "\r") then
+ he.cr = he.cr + 1
+ end
+ if startswith(line, "-") then
+ hunkactual.linessrc = hunkactual.linessrc + 1
+ elseif startswith(line, "+") then
+ hunkactual.linestgt = hunkactual.linestgt + 1
+ elseif startswith(line, "\\") then
+ -- nothing
+ else
+ hunkactual.linessrc = hunkactual.linessrc + 1
+ hunkactual.linestgt = hunkactual.linestgt + 1
+ end
+ table.insert(hunkinfo.text, line)
+ -- todo: handle \ No newline cases
+ else
+ warning(format("invalid hunk no.%d at %d for target file %s",
+ nexthunkno, lineno, files.target[nextfileno]))
+ -- add hunk status node
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ files.hunks[nextfileno][nexthunkno].invalid = true
+ all_ok = false
+ state = 'hunkskip'
+ end
+
+ -- check exit conditions
+ if hunkactual.linessrc > hunkinfo.linessrc or
+ hunkactual.linestgt > hunkinfo.linestgt
+ then
+ warning(format("extra hunk no.%d lines at %d for target %s",
+ nexthunkno, lineno, files.target[nextfileno]))
+ -- add hunk status node
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ files.hunks[nextfileno][nexthunkno].invalid = true
+ state = 'hunkskip'
+ elseif hunkinfo.linessrc == hunkactual.linessrc and
+ hunkinfo.linestgt == hunkactual.linestgt
+ then
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ state = 'hunkskip'
+
+ -- detect mixed window/unix line ends
+ local ends = files.hunkends[nextfileno]
+ if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) +
+ (ends.lf~=0 and 1 or 0) > 1
+ then
+ warning(format("inconsistent line ends in patch hunks for %s",
+ files.source[nextfileno]))
+ end
+ end
+ -- state is 'hunkbody' or 'hunkskip'
+ end
+
+ if state == 'hunkskip' then
+ if match_linerange(line) then
+ state = 'hunkhead'
+ elseif startswith(line, "--- ") then
+ state = 'filenames'
+ if debugmode and #files.source > 0 then
+ debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
+ files.source[nextfileno]))
+ end
+ end
+ -- state is 'hunkskip', 'hunkhead', or 'filenames'
+ end
+ local advance
+ if state == 'filenames' then
+ if startswith(line, "--- ") then
+ if fun.contains(files.source, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch for %s",
+ files.source[nextfileno+1]))
+ table.remove(files.source, nextfileno+1)
+ -- double source filename line is encountered
+ -- attempt to restart from this second line
+ end
+ -- Accept a space as a terminator, like GNU patch does.
+ -- Breaks patches containing filenames with spaces...
+ -- FIXME Figure out what does GNU patch do in those cases.
+ local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)")
+ if not match then
+ all_ok = false
+ warning(format("skipping invalid filename at line %d", lineno+1))
+ state = 'header'
+ else
+ if match_epoch(rest) then
+ files.epoch[nextfileno + 1] = true
+ end
+ table.insert(files.source, match)
+ end
+ elseif not startswith(line, "+++ ") then
+ if fun.contains(files.source, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch with no target for %s",
+ files.source[nextfileno+1]))
+ table.remove(files.source, nextfileno+1)
+ else
+ -- this should be unreachable
+ warning("skipping invalid target patch")
+ end
+ state = 'header'
+ else
+ if fun.contains(files.target, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch - double target at line %d",
+ lineno+1))
+ table.remove(files.source, nextfileno+1)
+ table.remove(files.target, nextfileno+1)
+ nextfileno = nextfileno - 1
+ -- double target filename line is encountered
+ -- switch back to header state
+ state = 'header'
+ else
+ -- Accept a space as a terminator, like GNU patch does.
+ -- Breaks patches containing filenames with spaces...
+ -- FIXME Figure out what does GNU patch do in those cases.
+ local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$"
+ local match, rest = line:match(re_filename)
+ if not match then
+ all_ok = false
+ warning(format(
+ "skipping invalid patch - no target filename at line %d",
+ lineno+1))
+ state = 'header'
+ else
+ table.insert(files.target, match)
+ nextfileno = nextfileno + 1
+ if match_epoch(rest) then
+ files.epoch[nextfileno] = true
+ end
+ nexthunkno = 0
+ table.insert(files.hunks, {})
+ table.insert(files.hunkends, table_copy(lineends))
+ table.insert(files.fileends, table_copy(lineends))
+ state = 'hunkhead'
+ advance = true
+ end
+ end
+ end
+ -- state is 'filenames', 'header', or ('hunkhead' with advance)
+ end
+ if not advance and state == 'hunkhead' then
+ local m1, m2, m3, m4 = match_linerange(line)
+ if not m1 then
+ if not fun.contains(files.hunks, nextfileno-1) then
+ all_ok = false
+ warning(format("skipping invalid patch with no hunks for file %s",
+ files.target[nextfileno]))
+ end
+ state = 'header'
+ else
+ hunkinfo.startsrc = tonumber(m1)
+ hunkinfo.linessrc = tonumber(m2 or 1)
+ hunkinfo.starttgt = tonumber(m3)
+ hunkinfo.linestgt = tonumber(m4 or 1)
+ hunkinfo.invalid = false
+ hunkinfo.text = {}
+
+ hunkactual.linessrc = 0
+ hunkactual.linestgt = 0
+
+ state = 'hunkbody'
+ nexthunkno = nexthunkno + 1
+ end
+ -- state is 'header' or 'hunkbody'
+ end
+ end
+ if state ~= 'hunkskip' then
+ warning(format("patch file incomplete - %s", filename))
+ all_ok = false
+ -- os.exit(?)
+ else
+ -- duplicated message when an eof is reached
+ if debugmode and #files.source > 0 then
+ debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
+ files.source[nextfileno]))
+ end
+ end
+
+ local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end
+ info(format("total files: %d total hunks: %d", #files.source, sum))
+ fp:close()
+ return files, all_ok
+end
+
+local function find_hunk(file, h, hno)
+ for fuzz=0,2 do
+ local lineno = h.startsrc
+ for i=0,#file do
+ local found = true
+ local location = lineno
+ for l, hline in ipairs(h.text) do
+ if l > fuzz then
+ -- todo: \ No newline at the end of file
+ if startswith(hline, " ") or startswith(hline, "-") then
+ local line = file[lineno]
+ lineno = lineno + 1
+ if not line or #line == 0 then
+ found = false
+ break
+ end
+ if endlstrip(line) ~= endlstrip(hline:sub(2)) then
+ found = false
+ break
+ end
+ end
+ end
+ end
+ if found then
+ local offset = location - h.startsrc - fuzz
+ if offset ~= 0 then
+ warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz)))
+ end
+ h.startsrc = location
+ h.starttgt = h.starttgt + offset
+ for _=1,fuzz do
+ table.remove(h.text, 1)
+ table.remove(h.text, #h.text)
+ end
+ return true
+ end
+ lineno = i
+ end
+ end
+ return false
+end
+
+local function load_file(filename)
+ local fp = assert(io.open(filename))
+ local file = {}
+ local readline = file_lines(fp)
+ while true do
+ local line = readline()
+ if not line then break end
+ table.insert(file, line)
+ end
+ fp:close()
+ return file
+end
+
+local function find_hunks(file, hunks)
+ for hno, h in ipairs(hunks) do
+ find_hunk(file, h, hno)
+ end
+end
+
+local function check_patched(file, hunks)
+ local lineno = 1
+ local ok, err = pcall(function()
+ if #file == 0 then
+ error('nomatch', 0)
+ end
+ for hno, h in ipairs(hunks) do
+ -- skip to line just before hunk starts
+ if #file < h.starttgt then
+ error('nomatch', 0)
+ end
+ lineno = h.starttgt
+ for _, hline in ipairs(h.text) do
+ -- todo: \ No newline at the end of file
+ if not startswith(hline, "-") and not startswith(hline, "\\") then
+ local line = file[lineno]
+ lineno = lineno + 1
+ if #line == 0 then
+ error('nomatch', 0)
+ end
+ if endlstrip(line) ~= endlstrip(hline:sub(2)) then
+ warning(format("file is not patched - failed hunk: %d", hno))
+ error('nomatch', 0)
+ end
+ end
+ end
+ end
+ end)
+ -- todo: display failed hunk, i.e. expected/found
+ return err ~= 'nomatch'
+end
+
+local function patch_hunks(srcname, tgtname, hunks)
+ local src = assert(io.open(srcname, "rb"))
+ local tgt = assert(io.open(tgtname, "wb"))
+
+ local src_readline = file_lines(src)
+
+ -- todo: detect linefeeds early - in apply_files routine
+ -- to handle cases when patch starts right from the first
+ -- line and no lines are processed. At the moment substituted
+ -- lineends may not be the same at the start and at the end
+ -- of patching. Also issue a warning about mixed lineends
+
+ local srclineno = 1
+ local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0}
+ for hno, h in ipairs(hunks) do
+ debug(format("processing hunk %d for file %s", hno, tgtname))
+ -- skip to line just before hunk starts
+ while srclineno < h.startsrc do
+ local line = src_readline()
+ -- Python 'U' mode works only with text files
+ if endswith(line, "\r\n") then
+ lineends["\r\n"] = lineends["\r\n"] + 1
+ elseif endswith(line, "\n") then
+ lineends["\n"] = lineends["\n"] + 1
+ elseif endswith(line, "\r") then
+ lineends["\r"] = lineends["\r"] + 1
+ end
+ tgt:write(line)
+ srclineno = srclineno + 1
+ end
+
+ for _,hline in ipairs(h.text) do
+ -- todo: check \ No newline at the end of file
+ if startswith(hline, "-") or startswith(hline, "\\") then
+ src_readline()
+ srclineno = srclineno + 1
+ else
+ if not startswith(hline, "+") then
+ src_readline()
+ srclineno = srclineno + 1
+ end
+ local line2write = hline:sub(2)
+ -- detect if line ends are consistent in source file
+ local sum = 0
+ for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end
+ if sum == 1 then
+ local newline
+ for k,v in pairs(lineends) do if v ~= 0 then newline = k end end
+ tgt:write(endlstrip(line2write) .. newline)
+ else -- newlines are mixed or unknown
+ tgt:write(line2write)
+ end
+ end
+ end
+ end
+ for line in src_readline do
+ tgt:write(line)
+ end
+ tgt:close()
+ src:close()
+ return true
+end
+
+local function strip_dirs(filename, strip)
+ if strip == nil then return filename end
+ for _=1,strip do
+ filename=filename:gsub("^[^/]*/", "")
+ end
+ return filename
+end
+
+local function write_new_file(filename, hunk)
+ local fh = io.open(filename, "wb")
+ if not fh then return false end
+ for _, hline in ipairs(hunk.text) do
+ local c = hline:sub(1,1)
+ if c ~= "+" and c ~= "-" and c ~= " " then
+ return false, "malformed patch"
+ end
+ fh:write(hline:sub(2))
+ end
+ fh:close()
+ return true
+end
+
+local function patch_file(source, target, epoch, hunks, strip, create_delete)
+ local create_file = false
+ if create_delete then
+ local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0
+ if is_src_epoch or source == "/dev/null" then
+ info(format("will create %s", target))
+ create_file = true
+ end
+ end
+ if create_file then
+ return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1])
+ end
+ source = strip_dirs(source, strip)
+ local f2patch = source
+ if not exists(f2patch) then
+ f2patch = strip_dirs(target, strip)
+ f2patch = fs.absolute_name(f2patch)
+ if not exists(f2patch) then --FIX:if f2patch nil
+ warning(format("source/target file does not exist\n--- %s\n+++ %s",
+ source, f2patch))
+ return false
+ end
+ end
+ if not isfile(f2patch) then
+ warning(format("not a file - %s", f2patch))
+ return false
+ end
+
+ source = f2patch
+
+ -- validate before patching
+ local file = load_file(source)
+ local hunkno = 1
+ local hunk = hunks[hunkno]
+ local hunkfind = {}
+ local validhunks = 0
+ local canpatch = false
+ local hunklineno
+ if not file then
+ return nil, "failed reading file " .. source
+ end
+
+ if create_delete then
+ if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then
+ local ok = os.remove(source)
+ if not ok then
+ return false
+ end
+ info(format("successfully removed %s", source))
+ return true
+ end
+ end
+
+ find_hunks(file, hunks)
+
+ local function process_line(line, lineno)
+ if not hunk or lineno < hunk.startsrc then
+ return false
+ end
+ if lineno == hunk.startsrc then
+ hunkfind = {}
+ for _,x in ipairs(hunk.text) do
+ if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then
+ hunkfind[#hunkfind+1] = endlstrip(x:sub(2))
+ end
+ end
+ hunklineno = 1
+
+ -- todo \ No newline at end of file
+ end
+ -- check hunks in source file
+ if lineno < hunk.startsrc + #hunkfind - 1 then
+ if endlstrip(line) == hunkfind[hunklineno] then
+ hunklineno = hunklineno + 1
+ else
+ debug(format("hunk no.%d doesn't match source file %s",
+ hunkno, source))
+ -- file may be already patched, but check other hunks anyway
+ hunkno = hunkno + 1
+ if hunkno <= #hunks then
+ hunk = hunks[hunkno]
+ return false
+ else
+ return true
+ end
+ end
+ end
+ -- check if processed line is the last line
+ if lineno == hunk.startsrc + #hunkfind - 1 then
+ debug(format("file %s hunk no.%d -- is ready to be patched",
+ source, hunkno))
+ hunkno = hunkno + 1
+ validhunks = validhunks + 1
+ if hunkno <= #hunks then
+ hunk = hunks[hunkno]
+ else
+ if validhunks == #hunks then
+ -- patch file
+ canpatch = true
+ return true
+ end
+ end
+ end
+ return false
+ end
+
+ local done = false
+ for lineno, line in ipairs(file) do
+ done = process_line(line, lineno)
+ if done then
+ break
+ end
+ end
+ if not done then
+ if hunkno <= #hunks and not create_file then
+ warning(format("premature end of source file %s at hunk %d",
+ source, hunkno))
+ return false
+ end
+ end
+ if validhunks < #hunks then
+ if check_patched(file, hunks) then
+ warning(format("already patched %s", source))
+ elseif not create_file then
+ warning(format("source file is different - %s", source))
+ return false
+ end
+ end
+ if not canpatch then
+ return true
+ end
+ local backupname = source .. ".orig"
+ if exists(backupname) then
+ warning(format("can't backup original file to %s - aborting",
+ backupname))
+ return false
+ end
+ local ok = os.rename(source, backupname)
+ if not ok then
+ warning(format("failed backing up %s when patching", source))
+ return false
+ end
+ patch_hunks(backupname, source, hunks)
+ info(format("successfully patched %s", source))
+ os.remove(backupname)
+ return true
+end
+
+function patch.apply_patch(the_patch, strip, create_delete)
+ local all_ok = true
+ local total = #the_patch.source
+ for fileno, source in ipairs(the_patch.source) do
+ local target = the_patch.target[fileno]
+ local hunks = the_patch.hunks[fileno]
+ local epoch = the_patch.epoch[fileno]
+ info(format("processing %d/%d:\t %s", fileno, total, source))
+ local ok = patch_file(source, target, epoch, hunks, strip, create_delete)
+ all_ok = all_ok and ok
+ end
+ -- todo: check for premature eof
+ return all_ok
+end
+
+return patch
diff --git a/src/luarocks/tools/tar.lua b/src/luarocks/tools/tar.lua
new file mode 100644
index 0000000..bac7b2a
--- /dev/null
+++ b/src/luarocks/tools/tar.lua
@@ -0,0 +1,191 @@
+
+--- A pure-Lua implementation of untar (unpacking .tar archives)
+local tar = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local fun = require("luarocks.fun")
+
+local blocksize = 512
+
+local function get_typeflag(flag)
+ if flag == "0" or flag == "\0" then return "file"
+ elseif flag == "1" then return "link"
+ elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU
+ elseif flag == "3" then return "character"
+ elseif flag == "4" then return "block"
+ elseif flag == "5" then return "directory"
+ elseif flag == "6" then return "fifo"
+ elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU
+ elseif flag == "x" then return "next file"
+ elseif flag == "g" then return "global extended header"
+ elseif flag == "L" then return "long name"
+ elseif flag == "K" then return "long link name"
+ end
+ return "unknown"
+end
+
+local function octal_to_number(octal)
+ local exp = 0
+ local number = 0
+ octal = octal:gsub("%s", "")
+ for i = #octal,1,-1 do
+ local digit = tonumber(octal:sub(i,i))
+ if not digit then
+ break
+ end
+ number = number + (digit * 8^exp)
+ exp = exp + 1
+ end
+ return number
+end
+
+local function checksum_header(block)
+ local sum = 256
+
+ if block:byte(1) == 0 then
+ return 0
+ end
+
+ for i = 1,148 do
+ local b = block:byte(i) or 0
+ sum = sum + b
+ end
+ for i = 157,500 do
+ local b = block:byte(i) or 0
+ sum = sum + b
+ end
+
+ return sum
+end
+
+local function nullterm(s)
+ return s:match("^[^%z]*")
+end
+
+local function read_header_block(block)
+ local header = {}
+ header.name = nullterm(block:sub(1,100))
+ header.mode = nullterm(block:sub(101,108)):gsub(" ", "")
+ header.uid = octal_to_number(nullterm(block:sub(109,116)))
+ header.gid = octal_to_number(nullterm(block:sub(117,124)))
+ header.size = octal_to_number(nullterm(block:sub(125,136)))
+ header.mtime = octal_to_number(nullterm(block:sub(137,148)))
+ header.chksum = octal_to_number(nullterm(block:sub(149,156)))
+ header.typeflag = get_typeflag(block:sub(157,157))
+ header.linkname = nullterm(block:sub(158,257))
+ header.magic = block:sub(258,263)
+ header.version = block:sub(264,265)
+ header.uname = nullterm(block:sub(266,297))
+ header.gname = nullterm(block:sub(298,329))
+ header.devmajor = octal_to_number(nullterm(block:sub(330,337)))
+ header.devminor = octal_to_number(nullterm(block:sub(338,345)))
+ header.prefix = block:sub(346,500)
+
+ -- if header.magic ~= "ustar " and header.magic ~= "ustar\0" then
+ -- return false, ("Invalid header magic %6x"):format(bestring_to_number(header.magic))
+ -- end
+ -- if header.version ~= "00" and header.version ~= " \0" then
+ -- return false, "Unknown version "..header.version
+ -- end
+ if header.typeflag == "unknown" then
+ if checksum_header(block) ~= header.chksum then
+ return false, "Failed header checksum"
+ end
+ end
+ return header
+end
+
+function tar.untar(filename, destdir)
+ assert(type(filename) == "string")
+ assert(type(destdir) == "string")
+
+ local tar_handle = io.open(filename, "rb")
+ if not tar_handle then return nil, "Error opening file "..filename end
+
+ local long_name, long_link_name
+ local ok, err
+ local make_dir = fun.memoize(fs.make_dir)
+ while true do
+ local block
+ repeat
+ block = tar_handle:read(blocksize)
+ until (not block) or block:byte(1) > 0
+ if not block then break end
+ if #block < blocksize then
+ ok, err = nil, "Invalid block size -- corrupted file?"
+ break
+ end
+
+ local header
+ header, err = read_header_block(block)
+ if not header then
+ ok = false
+ break
+ end
+
+ local file_data = ""
+ if header.size > 0 then
+ local nread = math.ceil(header.size / blocksize) * blocksize
+ file_data = tar_handle:read(header.size)
+ if nread > header.size then
+ tar_handle:seek("cur", nread - header.size)
+ end
+ end
+
+ if header.typeflag == "long name" then
+ long_name = nullterm(file_data)
+ elseif header.typeflag == "long link name" then
+ long_link_name = nullterm(file_data)
+ else
+ if long_name then
+ header.name = long_name
+ long_name = nil
+ end
+ if long_link_name then
+ header.name = long_link_name
+ long_link_name = nil
+ end
+ end
+ local pathname = dir.path(destdir, header.name)
+ pathname = fs.absolute_name(pathname)
+ if header.typeflag == "directory" then
+ ok, err = make_dir(pathname)
+ if not ok then
+ break
+ end
+ elseif header.typeflag == "file" then
+ local dirname = dir.dir_name(pathname)
+ if dirname ~= "" then
+ ok, err = make_dir(dirname)
+ if not ok then
+ break
+ end
+ end
+ local file_handle
+ file_handle, err = io.open(pathname, "wb")
+ if not file_handle then
+ ok = nil
+ break
+ end
+ file_handle:write(file_data)
+ file_handle:close()
+ fs.set_time(pathname, header.mtime)
+ if header.mode:match("[75]") then
+ fs.set_permissions(pathname, "exec", "all")
+ else
+ fs.set_permissions(pathname, "read", "all")
+ end
+ end
+ --[[
+ for k,v in pairs(header) do
+ util.printout("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\""))
+ end
+ util.printout()
+ --]]
+ end
+ tar_handle:close()
+ return ok, err
+end
+
+return tar
diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua
new file mode 100644
index 0000000..82d582f
--- /dev/null
+++ b/src/luarocks/tools/zip.lua
@@ -0,0 +1,531 @@
+
+--- A Lua implementation of .zip and .gz file compression and decompression,
+-- using only lzlib or lua-lzib.
+local zip = {}
+
+local zlib = require("zlib")
+local fs = require("luarocks.fs")
+local fun = require("luarocks.fun")
+local dir = require("luarocks.dir")
+
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local function shr(n, m)
+ return math.floor(n / 2^m)
+end
+
+local function shl(n, m)
+ return n * 2^m
+end
+local function lowbits(n, m)
+ return n % 2^m
+end
+
+local function mode_to_windowbits(mode)
+ if mode == "gzip" then
+ return 31
+ elseif mode == "zlib" then
+ return 0
+ elseif mode == "raw" then
+ return -15
+ end
+end
+
+-- zlib module can be provided by both lzlib and lua-lzib packages.
+-- Create a compatibility layer.
+local zlib_compress, zlib_uncompress, zlib_crc32
+if zlib._VERSION:match "^lua%-zlib" then
+ function zlib_compress(data, mode)
+ return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish"))
+ end
+
+ function zlib_uncompress(data, mode)
+ return (zlib.inflate(mode_to_windowbits(mode))(data))
+ end
+
+ function zlib_crc32(data)
+ return zlib.crc32()(data)
+ end
+elseif zlib._VERSION:match "^lzlib" then
+ function zlib_compress(data, mode)
+ return zlib.compress(data, -1, nil, mode_to_windowbits(mode))
+ end
+
+ function zlib_uncompress(data, mode)
+ return zlib.decompress(data, mode_to_windowbits(mode))
+ end
+
+ function zlib_crc32(data)
+ return zlib.crc32(zlib.crc32(), data)
+ end
+else
+ error("unknown zlib library", 0)
+end
+
+local function number_to_lestring(number, nbytes)
+ local out = {}
+ for _ = 1, nbytes do
+ local byte = number % 256
+ table.insert(out, string.char(byte))
+ number = (number - byte) / 256
+ end
+ return table.concat(out)
+end
+
+local function lestring_to_number(str)
+ local n = 0
+ local bytes = { string.byte(str, 1, #str) }
+ for b = 1, #str do
+ n = n + shl(bytes[b], (b-1)*8)
+ end
+ return math.floor(n)
+end
+
+local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4)
+local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4)
+local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4)
+local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4)
+
+--- Begin a new file to be stored inside the zipfile.
+-- @param self handle of the zipfile being written.
+-- @param filename filenome of the file to be added to the zipfile.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_open_new_file_in_zip(self, filename)
+ if self.in_open_file then
+ self:close_file_in_zip()
+ return nil
+ end
+ local lfh = {}
+ self.local_file_header = lfh
+ lfh.last_mod_file_time = 0 -- TODO
+ lfh.last_mod_file_date = 0 -- TODO
+ lfh.file_name_length = #filename
+ lfh.extra_field_length = 0
+ lfh.file_name = filename:gsub("\\", "/")
+ lfh.external_attr = shl(493, 16) -- TODO proper permissions
+ self.in_open_file = true
+ return true
+end
+
+--- Write data to the file currently being stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @param data string containing full contents of the file.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_write_file_in_zip(self, data)
+ if not self.in_open_file then
+ return nil
+ end
+ local lfh = self.local_file_header
+ local compressed = zlib_compress(data, "raw")
+ lfh.crc32 = zlib_crc32(data)
+ lfh.compressed_size = #compressed
+ lfh.uncompressed_size = #data
+ self.data = compressed
+ return true
+end
+
+--- Complete the writing of a file stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close_file_in_zip(self)
+ local zh = self.ziphandle
+
+ if not self.in_open_file then
+ return nil
+ end
+
+ -- Local file header
+ local lfh = self.local_file_header
+ lfh.offset = zh:seek()
+ zh:write(LOCAL_FILE_HEADER_SIGNATURE)
+ zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
+ zh:write(number_to_lestring(4, 2)) -- general purpose bit flag
+ zh:write(number_to_lestring(8, 2)) -- compression method: deflate
+ zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
+ zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+ zh:write(number_to_lestring(lfh.file_name_length, 2))
+ zh:write(number_to_lestring(lfh.extra_field_length, 2))
+ zh:write(lfh.file_name)
+
+ -- File data
+ zh:write(self.data)
+
+ -- Data descriptor
+ zh:write(DATA_DESCRIPTOR_SIGNATURE)
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+
+ table.insert(self.files, lfh)
+ self.in_open_file = false
+
+ return true
+end
+
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+local function zipwriter_add(self, file)
+ local fin
+ local ok, err = self:open_new_file_in_zip(file)
+ if not ok then
+ err = "error in opening "..file.." in zipfile"
+ else
+ fin = io.open(fs.absolute_name(file), "rb")
+ if not fin then
+ ok = false
+ err = "error opening "..file.." for reading"
+ end
+ end
+ if ok then
+ local data = fin:read("*a")
+ if not data then
+ err = "error reading "..file
+ ok = false
+ else
+ ok = self:write_file_in_zip(data)
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ end
+ if fin then
+ fin:close()
+ end
+ if ok then
+ ok = self:close_file_in_zip()
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ return ok == true, err
+end
+
+--- Complete the writing of the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close(self)
+ local zh = self.ziphandle
+
+ local central_directory_offset = zh:seek()
+
+ local size_of_central_directory = 0
+ -- Central directory structure
+ for _, lfh in ipairs(self.files) do
+ zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature
+ zh:write(number_to_lestring(3, 2)) -- version made by: UNIX
+ zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
+ zh:write(number_to_lestring(0, 2)) -- general purpose bit flag
+ zh:write(number_to_lestring(8, 2)) -- compression method: deflate
+ zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
+ zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+ zh:write(number_to_lestring(lfh.file_name_length, 2))
+ zh:write(number_to_lestring(lfh.extra_field_length, 2))
+ zh:write(number_to_lestring(0, 2)) -- file comment length
+ zh:write(number_to_lestring(0, 2)) -- disk number start
+ zh:write(number_to_lestring(0, 2)) -- internal file attributes
+ zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes
+ zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header
+ zh:write(lfh.file_name)
+ size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length
+ end
+
+ -- End of central directory record
+ zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature
+ zh:write(number_to_lestring(0, 2)) -- number of this disk
+ zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory
+ zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk
+ zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir
+ zh:write(number_to_lestring(size_of_central_directory, 4))
+ zh:write(number_to_lestring(central_directory_offset, 4))
+ zh:write(number_to_lestring(0, 2)) -- zip file comment length
+ zh:close()
+
+ return true
+end
+
+--- Return a zip handle open for writing.
+-- @param name filename of the zipfile to be created.
+-- @return a zip handle, or nil in case of error.
+function zip.new_zipwriter(name)
+
+ local zw = {}
+
+ zw.ziphandle = io.open(fs.absolute_name(name), "wb")
+ if not zw.ziphandle then
+ return nil
+ end
+ zw.files = {}
+ zw.in_open_file = false
+
+ zw.add = zipwriter_add
+ zw.close = zipwriter_close
+ zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip
+ zw.write_file_in_zip = zipwriter_write_file_in_zip
+ zw.close_file_in_zip = zipwriter_close_file_in_zip
+
+ return zw
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+function zip.zip(zipfile, ...)
+ local zw = zip.new_zipwriter(zipfile)
+ if not zw then
+ return nil, "error opening "..zipfile
+ end
+
+ local args = pack(...)
+ local ok, err
+ for i=1, args.n do
+ local file = args[i]
+ if fs.is_dir(file) then
+ for _, entry in pairs(fs.find(file)) do
+ local fullname = dir.path(file, entry)
+ if fs.is_file(fullname) then
+ ok, err = zw:add(fullname)
+ if not ok then break end
+ end
+ end
+ else
+ ok, err = zw:add(file)
+ if not ok then break end
+ end
+ end
+
+ zw:close()
+ return ok, err
+end
+
+
+local function ziptime_to_luatime(ztime, zdate)
+ local date = {
+ year = shr(zdate, 9) + 1980,
+ month = shr(lowbits(zdate, 9), 5),
+ day = lowbits(zdate, 5),
+ hour = shr(ztime, 11),
+ min = shr(lowbits(ztime, 11), 5),
+ sec = lowbits(ztime, 5) * 2,
+ }
+
+ if date.month == 0 then date.month = 1 end
+ if date.day == 0 then date.day = 1 end
+
+ return date
+end
+
+local function read_file_in_zip(zh, cdr)
+ local sig = zh:read(4)
+ if sig ~= LOCAL_FILE_HEADER_SIGNATURE then
+ return nil, "failed reading Local File Header signature"
+ end
+
+ -- Skip over the rest of the zip file header. See
+ -- zipwriter_close_file_in_zip for the format.
+ zh:seek("cur", 22)
+ local file_name_length = lestring_to_number(zh:read(2))
+ local extra_field_length = lestring_to_number(zh:read(2))
+ zh:read(file_name_length)
+ zh:read(extra_field_length)
+
+ local data = zh:read(cdr.compressed_size)
+
+ local uncompressed
+ if cdr.compression_method == 8 then
+ uncompressed = zlib_uncompress(data, "raw")
+ elseif cdr.compression_method == 0 then
+ uncompressed = data
+ else
+ return nil, "unknown compression method " .. cdr.compression_method
+ end
+
+ if #uncompressed ~= cdr.uncompressed_size then
+ return nil, "uncompressed size doesn't match"
+ end
+ if cdr.crc32 ~= zlib_crc32(uncompressed) then
+ return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed
+ end
+
+ return uncompressed
+end
+
+local function process_end_of_central_dir(zh)
+ local at, err = zh:seek("end", -22)
+ if not at then
+ return nil, err
+ end
+
+ while true do
+ local sig = zh:read(4)
+ if sig == END_OF_CENTRAL_DIR_SIGNATURE then
+ break
+ end
+ at = at - 1
+ local at1, err = zh:seek("set", at)
+ if at1 ~= at then
+ return nil, "Could not find End of Central Directory signature"
+ end
+ end
+
+ -- number of this disk (2 bytes)
+ -- number of the disk with the start of the central directory (2 bytes)
+ -- total number of entries in the central directory on this disk (2 bytes)
+ -- total number of entries in the central directory (2 bytes)
+ zh:seek("cur", 6)
+
+ local central_directory_entries = lestring_to_number(zh:read(2))
+
+ -- central directory size (4 bytes)
+ zh:seek("cur", 4)
+
+ local central_directory_offset = lestring_to_number(zh:read(4))
+
+ return central_directory_entries, central_directory_offset
+end
+
+local function process_central_dir(zh, cd_entries)
+
+ local files = {}
+
+ for i = 1, cd_entries do
+ local sig = zh:read(4)
+ if sig ~= CENTRAL_DIRECTORY_SIGNATURE then
+ return nil, "failed reading Central Directory signature"
+ end
+
+ local cdr = {}
+ files[i] = cdr
+
+ cdr.version_made_by = lestring_to_number(zh:read(2))
+ cdr.version_needed = lestring_to_number(zh:read(2))
+ cdr.bitflag = lestring_to_number(zh:read(2))
+ cdr.compression_method = lestring_to_number(zh:read(2))
+ cdr.last_mod_file_time = lestring_to_number(zh:read(2))
+ cdr.last_mod_file_date = lestring_to_number(zh:read(2))
+ cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date)
+ cdr.crc32 = lestring_to_number(zh:read(4))
+ cdr.compressed_size = lestring_to_number(zh:read(4))
+ cdr.uncompressed_size = lestring_to_number(zh:read(4))
+ cdr.file_name_length = lestring_to_number(zh:read(2))
+ cdr.extra_field_length = lestring_to_number(zh:read(2))
+ cdr.file_comment_length = lestring_to_number(zh:read(2))
+ cdr.disk_number_start = lestring_to_number(zh:read(2))
+ cdr.internal_attr = lestring_to_number(zh:read(2))
+ cdr.external_attr = lestring_to_number(zh:read(4))
+ cdr.offset = lestring_to_number(zh:read(4))
+ cdr.file_name = zh:read(cdr.file_name_length)
+ cdr.extra_field = zh:read(cdr.extra_field_length)
+ cdr.file_comment = zh:read(cdr.file_comment_length)
+ end
+ return files
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+function zip.unzip(zipfile)
+ zipfile = fs.absolute_name(zipfile)
+ local zh, err = io.open(zipfile, "rb")
+ if not zh then
+ return nil, err
+ end
+
+ local cd_entries, cd_offset = process_end_of_central_dir(zh)
+ if not cd_entries then
+ return nil, cd_offset
+ end
+
+ local ok, err = zh:seek("set", cd_offset)
+ if not ok then
+ return nil, err
+ end
+
+ local files, err = process_central_dir(zh, cd_entries)
+ if not files then
+ return nil, err
+ end
+
+ for _, cdr in ipairs(files) do
+ local file = cdr.file_name
+ if file:sub(#file) == "/" then
+ local ok, err = fs.make_dir(dir.path(fs.current_dir(), file))
+ if not ok then
+ return nil, err
+ end
+ else
+ local base = dir.dir_name(file)
+ if base ~= "" then
+ base = dir.path(fs.current_dir(), base)
+ if not fs.is_dir(base) then
+ local ok, err = fs.make_dir(base)
+ if not ok then
+ return nil, err
+ end
+ end
+ end
+
+ local ok, err = zh:seek("set", cdr.offset)
+ if not ok then
+ return nil, err
+ end
+
+ local contents, err = read_file_in_zip(zh, cdr)
+ if not contents then
+ return nil, err
+ end
+ local pathname = dir.path(fs.current_dir(), file)
+ local wf, err = io.open(pathname, "wb")
+ if not wf then
+ zh:close()
+ return nil, err
+ end
+ wf:write(contents)
+ wf:close()
+
+ if cdr.external_attr > 0 then
+ fs.set_permissions(pathname, "exec", "all")
+ else
+ fs.set_permissions(pathname, "read", "all")
+ end
+ fs.set_time(pathname, cdr.last_mod_luatime)
+ end
+ end
+ zh:close()
+ return true
+end
+
+function zip.gzip(input_filename, output_filename)
+ assert(type(input_filename) == "string")
+ assert(output_filename == nil or type(output_filename) == "string")
+
+ if not output_filename then
+ output_filename = input_filename .. ".gz"
+ end
+
+ local fn = fun.partial(fun.flip(zlib_compress), "gzip")
+ return fs.filter_file(fn, input_filename, output_filename)
+end
+
+function zip.gunzip(input_filename, output_filename)
+ assert(type(input_filename) == "string")
+ assert(output_filename == nil or type(output_filename) == "string")
+
+ if not output_filename then
+ output_filename = input_filename:gsub("%.gz$", "")
+ end
+
+ local fn = fun.partial(fun.flip(zlib_uncompress), "gzip")
+ return fs.filter_file(fn, input_filename, output_filename)
+end
+
+return zip