#!/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)