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
(limited to 'src')
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 x; 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