From 50a5a08416ed8ebcfe3d35eec534de503e18679e Mon Sep 17 00:00:00 2001 From: onefang Date: Mon, 13 Jan 2020 06:07:11 +1000 Subject: Experimental IAR unpacker. --- src/love/unpack_IAR.lua | 392 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100755 src/love/unpack_IAR.lua diff --git a/src/love/unpack_IAR.lua b/src/love/unpack_IAR.lua new file mode 100755 index 0000000..eeacc52 --- /dev/null +++ b/src/love/unpack_IAR.lua @@ -0,0 +1,392 @@ +#!/usr/bin/env luajit + +-- Pass the name of an IAR or a gitIAR file, this will unpack it into something resembling an in world inventory folder structure. + +-- TODO - make the output compatible with SledjHamr/docs/SledjHamr/love.txt + +local lxp = require "lxp" -- Lua-expat +lxp.lom = require "lxp/lom" +local posix = require "posix" + + +local args = {...} +local tarball = args[1] + + +assetsDir = "assets" +local assetTypes = +{ + [0] = "texture.jp2", -- YAY, Luajit is sane and allows the number 0 as a table key. + [1] = "sound.ogg", + [2] = "callingcard.txt", + [3] = "landmark.txt", + [4] = "script.lsl", + [5] = "clothing.txt", + [6] = "object.xml", + [7] = "notecard.txt", + [8] = "CATEGARY", -- CATEGORY + [9] = "ROOT", -- ROOT_CATEGORY + [10] = "script.lsl", + [11] = "BYTECODE", -- LSL_BYTECODE + [12] = "texture.tga", + [13] = "bodypart.txt", + [14] = "TRASH", -- TRASH + [15] = "SNAPSHOTS", -- SNAPSHOT_CATEGORY + [16] = "LOSTANDFOUND", -- LOST_AND_FOUND + [17] = "sound.wav", + [18] = "image.tga", + [19] = "image.jpeg", + [20] = "animation.bvh", + [21] = "gesture.txt", + [22] = "SIMSTATE", -- SIMSTATE + [23] = "DUNNO", + [24] = "link", --[[ + So the AssetID in this one is the AssetID for the thing it links to - + Current Outfit__6ff2f548-abfb-245f-d8cc-a72e7a7a08ef/Girl bold base Hair__4692489c-4a70-4b0c-acd4-87943a54436b.xml + 56d40470-4501-4a1f-8c3a-b22bf1fd3893 + Teen Girl Avatar__bc4c51e9-08a9-4f3b-89f5-de9dc68c8050/Girl bold base Hair__56d40470-4501-4a1f-8c3a-b22bf1fd3893.xml + 104d875b-d8c9-49bf-a499-47728fdec164 + assets/104d875b-d8c9-49bf-a499-47728fdec164_bodypart.txt + But I strip all those filename UUIDs, and the link doesn't include the folder details. + I'll have to keep track of the stripped UUIDs -> file mappings. + Which I wanted to do anyway, coz viewers and such will be asking for UUIDs. + ]] +} + +-- https://en.wikipedia.org/wiki/Filename - Reserved characters and words +-- OpenSim makes &#xx; out of characters it doesn't like. +-- Unix wants &#; escaped in command lines. We are quoting them anyway, but let's be safe. +local sanitiser = +{ + ["["] = "{", + ["]"] = "}", + ["\\"] = "_", + ["/"] = "_", + ["<"] = "_", + [">"] = "_", + [":"] = "_", + [";"] = "_", + ["*"] = "_", + ["?"] = "_", + ["'"] = "_", + ['"'] = "_", + ["|"] = "!", + ["@"] = "^", + ["#"] = "^", + ["$"] = "^", +-- ["%"] = "^", -- Only a problem in RT-11 + ["&"] = "^", + [" "] = "_", +-- ["."] = "-", -- Position and operating system dependant. + ["%c"] = "^", + ["\x60"] = "_", + ["\x7F"] = "_", +} +local sanitise = function (s) + return s:gsub("(.)", sanitiser) +end + +local uniqueTable = function (tab) + local r = {} + local l + table.sort(tab) + for k, v in pairs(tab) do + if v ~= l then + table.insert(r, v) + l = v + end + end + return r +end + +-- Use this to dump a table to a string. +dumpTable = function (table, space, name) + local r = "" + if "" == space then r = r .. space .. name .. " =\n" else r = r .. space .. "[" .. name .. "] =\n" end + r = r .. space .. "{\n" + r = r .. dumpTableSub(table, space .. " ") + if "" == space then r = r .. space .. "}\n" else r = r .. space .. "},\n" end + return r +end +dumpTableSub = function (table, space) + local r = "" + for k, v in pairs(table) do + if type(k) == "string" then k = '"' .. k .. '"' end + if type(v) == "table" then + r = r .. dumpTable(v, space, k) + elseif type(v) == "string" then + r = r .. space .. "[" .. k .. "] = '" .. v .. "';\n" + elseif type(v) == "function" then + r = r .. space .. "[" .. k .. "] = function ();\n" + elseif type(v) == "userdata" then + r = r .. space .. "userdata " .. "[" .. k .. "];\n" + elseif type(v) == "boolean" then + if (v) then + r = r .. space .. "boolean " .. "[" .. k .. "] = true;\n" + else + r = r .. space .. "boolean " .. "[" .. k .. "] = false;\n" + end + else + r = r .. space .. "[" .. k .. "] = " .. v .. ";\n" + end + end + return r +end + + + +local getXML = function (file) + local s = "" + local iFile, e = io.open(file, "r") + if nil == iFile then print("ERROR opening file " .. file .. " - " .. e); return {} end + for l in iFile:lines() do + -- Strip white space from each end of the line. + l = string.gsub(l, '^%s*(.*)', '%1') + l = string.gsub(l, '(.-)%s*$', '%1') + if " name.0 name.1 ... + local c = 0 + local n = l + if (nil ~= o[tag][l]) then + n = l .. '.' .. c + o[tag][n] = o[tag][l] + o[tag][l] = nil + end + while nil ~= o[tag][n] do + c = c + 1 + n = l .. '.' .. c + end + o[tag][n] = w + end + else + o[tag][k] = v + end + end + end + else +--print("NO TAG!!") +--printTable(tab, "", "NO TAG!!") +--o = tab + end + return o +end + +--[[ Silly SL allowed duplicate names in inventories, but not in object contents. + In contents they automatically get renamed to "name 1", even if they are different UUIDs. + The duplicated file__UUID.xml files in the IAR have different UUIDs. (ID field in the .xml) + "cp --backup=numbered " results in file.ext.~1~ +]] +deDup = function (file, ext) + if nil == ext then ext = "" else ext = '.' .. ext end + local count = 0 + local st = posix.stat(file .. ext, "type") + while nil ~= st do + count = count + 1 + st = posix.stat(file .. ' ' .. count .. ext, "type") + end + if 0 < count then + print("Dupe detected " .. file .. ext) + file = file .. ' ' .. count + end + return file .. ext +end + +writeTable = function (tab, file, title) + os.execute('touch "' .. file .. '"') + local iFile, e = io.open(file, "w+") + if nil == iFile then print("ERROR opening file " .. file .. " - " .. e); return false end + iFile:write(dumpTable(tab, "", title)) + iFile:close() + return true +end + +-- TODO - should sanitise all these names. The same way SledjHamr does. +local stripUUID = "%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x" +convertObject = function (source, dest, name) + -- It's a complex object, parse it's asset file and deal with it. + local tb = getXML(source) + if 0 == #tb then return end + local tbl = reduceXML(tb) + tbl.Name = name + tbl.AssetID = posix.basename(source):gsub("__" .. stripUUID, ""):gsub(".xml", "") + tbl.AssetType = 6 + + local ddest = deDup(dest .. "/" .. name) + posix.mkdir(ddest) + if not writeTable(tbl, deDup(dest .. "/." .. name, "lua"), tbl.AssetID) then return end + + if (nil ~= tbl.SceneObjectGroup) then + if (nil ~= tbl.SceneObjectGroup.RootPart) + and (nil ~= tbl.SceneObjectGroup.RootPart.SceneObjectPart) + and (nil ~= tbl.SceneObjectGroup.RootPart.SceneObjectPart.TaskInventory) then + for k, v in pairs(tbl.SceneObjectGroup.RootPart.SceneObjectPart.TaskInventory) do + if "TaskInventoryItem" == k:sub(1, 17) then + local ext = assetTypes[tonumber(v.Type)] + local fl = assetsDir .. "/" .. v.AssetID.UUID .. "_" .. ext + -- If it's an object.xml, parse it recursively. + if "object.xml" ~= ext then + os.execute('cp --backup=numbered "' .. fl .. '" "' .. deDup(ddest .. "/" .. sanitise(v.Name), ext) .. '"') + else + convertObject(fl, ddest, sanitise(v.Name)) + end + end + end + end + + if (nil ~= tbl.SceneObjectGroup.OtherParts) then + for j, u in pairs(tbl.SceneObjectGroup.OtherParts) do + if "Part" == j:sub(1, 4) then + if (nil ~= u.SceneObjectPart) and (nil ~= u.SceneObjectPart.TaskInventory) then + local udest = ddest .. '/' .. sanitise(u.SceneObjectPart.Name) + posix.mkdir(udest) + for k, v in pairs(u.SceneObjectPart.TaskInventory) do + if "TaskInventoryItem" == k:sub(1, 17) then + local ext = assetTypes[tonumber(v.Type)] + local fl = assetsDir .. "/" .. v.AssetID.UUID .. "_" .. ext + -- If it's an object.xml, parse it recursively. + if "object.xml" ~= ext then + os.execute('cp --backup=numbered "' .. fl .. '" "' .. deDup(udest .. '/' .. sanitise(v.Name), ext) .. '"') + else + convertObject(fl, ddest, sanatise(v.Name)) + end + end + end + end + end + end + end + end +end + +convertFiles = function (st, source) + local name = posix.basename(source):gsub("__" .. stripUUID, ""):gsub(".xml", "") + local dest = "IARs/" .. posix.dirname( source):gsub("__" .. stripUUID, "") + local object = {} + + -- Sanitise the directory name. + local d = dest + dest = "" + while "." ~= d do + dest = sanitise(posix.basename(d)) .. "/" .. dest + d = posix.dirname(d) + end + dest = dest:sub(1, -2) + + posix.mkdir(dest) + if "regular" ~= st then return end + + -- Parse the IAR XML file. which we assume this is. + for k, v in pairs(getXML(source)) do + if nil ~= v.tag then object[v.tag] = v[1] end + end + + -- Double check we got an asset ID. + if nil ~= object.AssetID then + -- In SledjHamr these .lua table files are called .omg, and have a different structure. Call these ones .liar / .loar? .lOS? + local tp = assetTypes[tonumber(object.AssetType)] + if nil == tp then tp = "UNKNOWN" end + local asset = assetsDir .. "/" .. object.AssetID .. "_" .. tp + + if "object.xml" ~= tp then + -- If it's an ordinary asset, create the metadata file and copy the asset file. + writeTable(object, deDup(dest .. "/." .. sanitise(name), "lua"), object.AssetID) + os.execute('cp --backup=numbered "' .. asset .. '" "' .. deDup(dest .. "/" .. sanitise(name), tp) .. '"') + else + convertObject(asset, dest, sanitise(object.Name)) + end + + end +end + +scanDir = function (dir, func) + local files, errstr, errno = posix.dir(dir) + if files then + for a,b in ipairs(files) do + if ("." ~= b) and (".." ~= b) then + local file = dir .. "/" .. b + local st = posix.stat(file, "type") + if nil ~= st then + if nil ~= func then func(st, file) end + if "directory" == st then scanDir(dir .. "/" .. b, func) end + else + print("NOT" .. " " .. file) + end + end + end + else + print(errstr) + end +end + + +if 0 ~= #args then +end + + +local e = tarball:gsub("(.+)%.(%a+)", "%2") +local archiver = "z" +local IARname = tarball:gsub("(.+)%.(%a+)", "%1") +if ("iar" ~= e:lower()) then + archiver = "a" + -- Assume the file is a tarball with ".tar." as part of the name. + IARname = IARname:gsub("(.+)%.(%a+)", "%1") +end +assetsDir = IARname .. "/assets" +os.execute("rm -rf " .. IARname) +posix.mkdir(IARname) +os.execute("tar -x" .. archiver .. "f " .. tarball .. " -C " .. IARname) + + +posix.mkdir("IARs") +os.execute("rm -rf IARs/" .. IARname) +posix.mkdir("IARs/" .. IARname) +posix.mkdir("IARs/" .. IARname .."/inventory") + +local inv = IARname .. "/inventory" +local st = posix.stat(inv, "type") +if nil == st then + assetsDir = IARname .. "/" .. assetsDir + inv = IARname .. "/" .. inv + posix.mkdir("IARs/" .. IARname) + posix.mkdir("IARs/" .. IARname .."/" .. IARname) + posix.mkdir("IARs/" .. IARname .."/" .. IARname .. "/inventory") +end + +scanDir(inv, convertFiles) +--os.execute("rm -rf " .. IARname) -- cgit v1.1