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 ") 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)