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