forked from Hyperion/HyperionOS
188 lines
5.4 KiB
Lua
188 lines
5.4 KiB
Lua
---@diagnostic disable: undefined-global
|
||
-- unpack_tar.lua
|
||
-- Fully working TAR unpacker for ComputerCraft
|
||
-- Handles Windows Explorer TAR bug (duplicated path segments)
|
||
|
||
local function octal_to_number(str)
|
||
str = str:gsub("%z", ""):match("^%s*(.-)%s*$")
|
||
return tonumber(str, 8) or 0
|
||
end
|
||
|
||
---------------------------------------------------------------------
|
||
-- Fix Windows Explorer TAR bug: repeated prefix segments in name
|
||
---------------------------------------------------------------------
|
||
local function dedupe_path(path)
|
||
-- Example:
|
||
-- Build/boot/Build/boot/cct/kernel.lua
|
||
-- -> Build/boot/cct/kernel.lua
|
||
|
||
local parts = {}
|
||
for p in path:gmatch("[^/]+") do table.insert(parts, p) end
|
||
|
||
-- Try all possible prefix lengths
|
||
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
|
||
-- Remove one repeated prefix
|
||
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
|
||
|
||
---------------------------------------------------------------------
|
||
-- Directory tree helpers
|
||
---------------------------------------------------------------------
|
||
|
||
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
|
||
|
||
---------------------------------------------------------------------
|
||
-- TAR Unpacker
|
||
---------------------------------------------------------------------
|
||
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)
|
||
|
||
-- End of archive (null block)
|
||
if header:match("^\0+$") then break end
|
||
|
||
-- Read name + ustar prefix
|
||
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
|
||
|
||
-- Normalize path
|
||
name = name:gsub("^%./", ""):gsub("/+", "/")
|
||
|
||
-- Fix Windows Explorer TAR bug
|
||
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
|
||
|
||
-- Determine if it’s a directory
|
||
local is_dir = typeflag == "5" or name:sub(-1) == "/"
|
||
|
||
-- Remove trailing slash for hierarchical processing
|
||
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
|
||
|
||
---------------------------------------------------------------------
|
||
-- FS output (ComputerCraft)
|
||
---------------------------------------------------------------------
|
||
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
|
||
|
||
---------------------------------------------------------------------
|
||
-- Entry point
|
||
---------------------------------------------------------------------
|
||
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)
|