158 lines
3.9 KiB
Plaintext
158 lines
3.9 KiB
Plaintext
|
|
local function octal_to_number(str)
|
|
str = str:gsub("%z", ""):match("^%s*(.-)%s*$")
|
|
return tonumber(str, 8) or 0
|
|
end
|
|
|
|
local function dedupe_path(path)
|
|
|
|
local parts = {}
|
|
for p in path:gmatch("[^/]+") do table.insert(parts, p) end
|
|
|
|
for prefix_len = 1, math.floor(#parts / 2) do
|
|
local ok = true
|
|
for i = 1, prefix_len do
|
|
if parts[i] ~= parts[i + prefix_len] then
|
|
ok = false
|
|
break
|
|
end
|
|
end
|
|
if ok then
|
|
local cleaned = {}
|
|
for i = 1, #parts - prefix_len do
|
|
cleaned[#cleaned + 1] = parts[i + prefix_len]
|
|
end
|
|
return table.concat(cleaned, "/")
|
|
end
|
|
end
|
|
|
|
return path
|
|
end
|
|
|
|
|
|
local function make_dirs(root, path)
|
|
local cur = root
|
|
for part in path:gmatch("([^/]+)/") do
|
|
if not cur[part] then
|
|
cur[part] = { __type = "dir", __entries = {} }
|
|
end
|
|
cur = cur[part].__entries
|
|
end
|
|
return cur
|
|
end
|
|
|
|
local function flatten(node, prefix)
|
|
local out = {}
|
|
prefix = prefix or ""
|
|
|
|
for name, obj in pairs(node) do
|
|
local full = prefix .. name
|
|
if obj.__type == "file" then
|
|
out[#out+1] = {
|
|
name = full,
|
|
type = "file",
|
|
contents = obj.__contents
|
|
}
|
|
elseif obj.__type == "dir" then
|
|
out[#out+1] = {
|
|
name = full .. "/",
|
|
type = "dir",
|
|
contents = flatten(obj.__entries, full .. "/")
|
|
}
|
|
end
|
|
end
|
|
|
|
return out
|
|
end
|
|
|
|
local function unpack_tar(tarstr)
|
|
local i = 1
|
|
local len = #tarstr
|
|
local root = {}
|
|
|
|
while i + 512 <= len do
|
|
local header = tarstr:sub(i, i + 511)
|
|
|
|
if header:match("^\0+$") then break end
|
|
|
|
local name_raw = header:sub(1, 100):gsub("%z.*", "")
|
|
local prefix_raw = header:sub(346, 500):gsub("%z.*", "")
|
|
|
|
local name
|
|
if prefix_raw ~= "" then
|
|
name = prefix_raw .. "/" .. name_raw
|
|
else
|
|
name = name_raw
|
|
end
|
|
|
|
name = name:gsub("^%./", ""):gsub("/+", "/")
|
|
|
|
name = dedupe_path(name)
|
|
|
|
local size = octal_to_number(header:sub(125,136))
|
|
local typeflag = header:sub(157,157)
|
|
|
|
i = i + 512
|
|
|
|
local contents = tarstr:sub(i, i + size - 1)
|
|
local pad = (512 - (size % 512)) % 512
|
|
i = i + size + pad
|
|
|
|
if name == "" then goto continue end
|
|
|
|
local is_dir = typeflag == "5" or name:sub(-1) == "/"
|
|
|
|
local clean_name = name:gsub("/$", "")
|
|
if clean_name == "" then goto continue end
|
|
|
|
local parent_path = clean_name:match("(.+)/")
|
|
local fname = clean_name:match("([^/]+)$")
|
|
if not fname then goto continue end
|
|
|
|
local parent = root
|
|
if parent_path then
|
|
parent = make_dirs(root, parent_path .. "/")
|
|
end
|
|
|
|
if is_dir then
|
|
parent[fname] = parent[fname] or { __type = "dir", __entries = {} }
|
|
else
|
|
parent[fname] = { __type = "file", __contents = contents }
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
return flatten(root)
|
|
end
|
|
|
|
local function write_directory(prefix, items)
|
|
for _, v in ipairs(items) do
|
|
if v.type == "dir" then
|
|
fs.makeDir(prefix..v.name)
|
|
write_directory(prefix, v.contents)
|
|
elseif v.type == "file" then
|
|
local file = fs.open(prefix..v.name, "w")
|
|
file.write(v.contents)
|
|
file.close()
|
|
end
|
|
end
|
|
end
|
|
|
|
local in_tar = ({...})[1]
|
|
local out_dir = ({...})[2]
|
|
|
|
if not in_tar or not out_dir then
|
|
print("Usage: unpack_tar <tarfile> <output_dir>")
|
|
return
|
|
end
|
|
|
|
local f = fs.open(in_tar, "r")
|
|
local tarstr = f.readAll()
|
|
f.close()
|
|
|
|
local list = unpack_tar(tarstr)
|
|
write_directory(out_dir, list)
|
|
|
|
print("TAR extracted into: " .. out_dir)
|