diff options
author | onefang | 2020-01-13 06:07:11 +1000 |
---|---|---|
committer | onefang | 2020-01-13 06:07:11 +1000 |
commit | 50a5a08416ed8ebcfe3d35eec534de503e18679e (patch) | |
tree | 981e2d4527d10fda8941ad263b41d00380ab426c | |
parent | Move luajit.pc to lib directory. (diff) | |
download | SledjHamr-50a5a08416ed8ebcfe3d35eec534de503e18679e.zip SledjHamr-50a5a08416ed8ebcfe3d35eec534de503e18679e.tar.gz SledjHamr-50a5a08416ed8ebcfe3d35eec534de503e18679e.tar.bz2 SledjHamr-50a5a08416ed8ebcfe3d35eec534de503e18679e.tar.xz |
Experimental IAR unpacker.
-rwxr-xr-x | src/love/unpack_IAR.lua | 392 |
1 files changed, 392 insertions, 0 deletions
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 @@ | |||
1 | #!/usr/bin/env luajit | ||
2 | |||
3 | -- Pass the name of an IAR or a gitIAR file, this will unpack it into something resembling an in world inventory folder structure. | ||
4 | |||
5 | -- TODO - make the output compatible with SledjHamr/docs/SledjHamr/love.txt | ||
6 | |||
7 | local lxp = require "lxp" -- Lua-expat | ||
8 | lxp.lom = require "lxp/lom" | ||
9 | local posix = require "posix" | ||
10 | |||
11 | |||
12 | local args = {...} | ||
13 | local tarball = args[1] | ||
14 | |||
15 | |||
16 | assetsDir = "assets" | ||
17 | local assetTypes = | ||
18 | { | ||
19 | [0] = "texture.jp2", -- YAY, Luajit is sane and allows the number 0 as a table key. | ||
20 | [1] = "sound.ogg", | ||
21 | [2] = "callingcard.txt", | ||
22 | [3] = "landmark.txt", | ||
23 | [4] = "script.lsl", | ||
24 | [5] = "clothing.txt", | ||
25 | [6] = "object.xml", | ||
26 | [7] = "notecard.txt", | ||
27 | [8] = "CATEGARY", -- CATEGORY | ||
28 | [9] = "ROOT", -- ROOT_CATEGORY | ||
29 | [10] = "script.lsl", | ||
30 | [11] = "BYTECODE", -- LSL_BYTECODE | ||
31 | [12] = "texture.tga", | ||
32 | [13] = "bodypart.txt", | ||
33 | [14] = "TRASH", -- TRASH | ||
34 | [15] = "SNAPSHOTS", -- SNAPSHOT_CATEGORY | ||
35 | [16] = "LOSTANDFOUND", -- LOST_AND_FOUND | ||
36 | [17] = "sound.wav", | ||
37 | [18] = "image.tga", | ||
38 | [19] = "image.jpeg", | ||
39 | [20] = "animation.bvh", | ||
40 | [21] = "gesture.txt", | ||
41 | [22] = "SIMSTATE", -- SIMSTATE | ||
42 | [23] = "DUNNO", | ||
43 | [24] = "link", --[[ | ||
44 | So the AssetID in this one is the AssetID for the thing it links to - | ||
45 | Current Outfit__6ff2f548-abfb-245f-d8cc-a72e7a7a08ef/Girl bold base Hair__4692489c-4a70-4b0c-acd4-87943a54436b.xml | ||
46 | <AssetID>56d40470-4501-4a1f-8c3a-b22bf1fd3893</AssetID> | ||
47 | Teen Girl Avatar__bc4c51e9-08a9-4f3b-89f5-de9dc68c8050/Girl bold base Hair__56d40470-4501-4a1f-8c3a-b22bf1fd3893.xml | ||
48 | <AssetID>104d875b-d8c9-49bf-a499-47728fdec164</AssetID> | ||
49 | assets/104d875b-d8c9-49bf-a499-47728fdec164_bodypart.txt | ||
50 | But I strip all those filename UUIDs, and the link doesn't include the folder details. | ||
51 | I'll have to keep track of the stripped UUIDs -> file mappings. | ||
52 | Which I wanted to do anyway, coz viewers and such will be asking for UUIDs. | ||
53 | ]] | ||
54 | } | ||
55 | |||
56 | -- https://en.wikipedia.org/wiki/Filename - Reserved characters and words | ||
57 | -- OpenSim makes &#xx; out of characters it doesn't like. | ||
58 | -- Unix wants &#; escaped in command lines. We are quoting them anyway, but let's be safe. | ||
59 | local sanitiser = | ||
60 | { | ||
61 | ["["] = "{", | ||
62 | ["]"] = "}", | ||
63 | ["\\"] = "_", | ||
64 | ["/"] = "_", | ||
65 | ["<"] = "_", | ||
66 | [">"] = "_", | ||
67 | [":"] = "_", | ||
68 | [";"] = "_", | ||
69 | ["*"] = "_", | ||
70 | ["?"] = "_", | ||
71 | ["'"] = "_", | ||
72 | ['"'] = "_", | ||
73 | ["|"] = "!", | ||
74 | ["@"] = "^", | ||
75 | ["#"] = "^", | ||
76 | ["$"] = "^", | ||
77 | -- ["%"] = "^", -- Only a problem in RT-11 | ||
78 | ["&"] = "^", | ||
79 | [" "] = "_", | ||
80 | -- ["."] = "-", -- Position and operating system dependant. | ||
81 | ["%c"] = "^", | ||
82 | ["\x60"] = "_", | ||
83 | ["\x7F"] = "_", | ||
84 | } | ||
85 | local sanitise = function (s) | ||
86 | return s:gsub("(.)", sanitiser) | ||
87 | end | ||
88 | |||
89 | local uniqueTable = function (tab) | ||
90 | local r = {} | ||
91 | local l | ||
92 | table.sort(tab) | ||
93 | for k, v in pairs(tab) do | ||
94 | if v ~= l then | ||
95 | table.insert(r, v) | ||
96 | l = v | ||
97 | end | ||
98 | end | ||
99 | return r | ||
100 | end | ||
101 | |||
102 | -- Use this to dump a table to a string. | ||
103 | dumpTable = function (table, space, name) | ||
104 | local r = "" | ||
105 | if "" == space then r = r .. space .. name .. " =\n" else r = r .. space .. "[" .. name .. "] =\n" end | ||
106 | r = r .. space .. "{\n" | ||
107 | r = r .. dumpTableSub(table, space .. " ") | ||
108 | if "" == space then r = r .. space .. "}\n" else r = r .. space .. "},\n" end | ||
109 | return r | ||
110 | end | ||
111 | dumpTableSub = function (table, space) | ||
112 | local r = "" | ||
113 | for k, v in pairs(table) do | ||
114 | if type(k) == "string" then k = '"' .. k .. '"' end | ||
115 | if type(v) == "table" then | ||
116 | r = r .. dumpTable(v, space, k) | ||
117 | elseif type(v) == "string" then | ||
118 | r = r .. space .. "[" .. k .. "] = '" .. v .. "';\n" | ||
119 | elseif type(v) == "function" then | ||
120 | r = r .. space .. "[" .. k .. "] = function ();\n" | ||
121 | elseif type(v) == "userdata" then | ||
122 | r = r .. space .. "userdata " .. "[" .. k .. "];\n" | ||
123 | elseif type(v) == "boolean" then | ||
124 | if (v) then | ||
125 | r = r .. space .. "boolean " .. "[" .. k .. "] = true;\n" | ||
126 | else | ||
127 | r = r .. space .. "boolean " .. "[" .. k .. "] = false;\n" | ||
128 | end | ||
129 | else | ||
130 | r = r .. space .. "[" .. k .. "] = " .. v .. ";\n" | ||
131 | end | ||
132 | end | ||
133 | return r | ||
134 | end | ||
135 | |||
136 | |||
137 | |||
138 | local getXML = function (file) | ||
139 | local s = "" | ||
140 | local iFile, e = io.open(file, "r") | ||
141 | if nil == iFile then print("ERROR opening file " .. file .. " - " .. e); return {} end | ||
142 | for l in iFile:lines() do | ||
143 | -- Strip white space from each end of the line. | ||
144 | l = string.gsub(l, '^%s*(.*)', '%1') | ||
145 | l = string.gsub(l, '(.-)%s*$', '%1') | ||
146 | if "<?xml " ~= l:sub(1, 6) then s = s .. l end | ||
147 | end | ||
148 | iFile:close() | ||
149 | return lxp.lom.parse(s) | ||
150 | end | ||
151 | |||
152 | reduceXML = function(tab) | ||
153 | local o = {} | ||
154 | if nil ~= tab.attr then | ||
155 | if 0 == #(tab.attr) then tab.attr = nil end | ||
156 | end | ||
157 | |||
158 | if nil ~= tab.tag then | ||
159 | local tag = tab.tag | ||
160 | tab.tag = nil | ||
161 | if 1 == #tab then | ||
162 | if "table" == type(tab[1]) then | ||
163 | o[tag] = {} | ||
164 | local n = reduceXML(tab[1]) | ||
165 | for l, w in pairs(n) do | ||
166 | local c = 0 | ||
167 | local n = l | ||
168 | while nil ~= o[tag][n] do | ||
169 | n = l .. '.' .. c | ||
170 | c = c + 1 | ||
171 | end | ||
172 | o[tag][n] = w | ||
173 | end | ||
174 | else | ||
175 | o[tag] = tab[1] | ||
176 | end | ||
177 | else | ||
178 | o[tag] = {} | ||
179 | for k, v in pairs(tab) do | ||
180 | if "table" == type(v) then | ||
181 | local n = reduceXML(v) | ||
182 | for l, w in pairs(n) do | ||
183 | -- If there are multiple of the same name -> name.0 name.1 ... | ||
184 | local c = 0 | ||
185 | local n = l | ||
186 | if (nil ~= o[tag][l]) then | ||
187 | n = l .. '.' .. c | ||
188 | o[tag][n] = o[tag][l] | ||
189 | o[tag][l] = nil | ||
190 | end | ||
191 | while nil ~= o[tag][n] do | ||
192 | c = c + 1 | ||
193 | n = l .. '.' .. c | ||
194 | end | ||
195 | o[tag][n] = w | ||
196 | end | ||
197 | else | ||
198 | o[tag][k] = v | ||
199 | end | ||
200 | end | ||
201 | end | ||
202 | else | ||
203 | --print("NO TAG!!") | ||
204 | --printTable(tab, "", "NO TAG!!") | ||
205 | --o = tab | ||
206 | end | ||
207 | return o | ||
208 | end | ||
209 | |||
210 | --[[ Silly SL allowed duplicate names in inventories, but not in object contents. | ||
211 | In contents they automatically get renamed to "name 1", even if they are different UUIDs. | ||
212 | The duplicated file__UUID.xml files in the IAR have different UUIDs. (ID field in the .xml) | ||
213 | "cp --backup=numbered " results in file.ext.~1~ | ||
214 | ]] | ||
215 | deDup = function (file, ext) | ||
216 | if nil == ext then ext = "" else ext = '.' .. ext end | ||
217 | local count = 0 | ||
218 | local st = posix.stat(file .. ext, "type") | ||
219 | while nil ~= st do | ||
220 | count = count + 1 | ||
221 | st = posix.stat(file .. ' ' .. count .. ext, "type") | ||
222 | end | ||
223 | if 0 < count then | ||
224 | print("Dupe detected " .. file .. ext) | ||
225 | file = file .. ' ' .. count | ||
226 | end | ||
227 | return file .. ext | ||
228 | end | ||
229 | |||
230 | writeTable = function (tab, file, title) | ||
231 | os.execute('touch "' .. file .. '"') | ||
232 | local iFile, e = io.open(file, "w+") | ||
233 | if nil == iFile then print("ERROR opening file " .. file .. " - " .. e); return false end | ||
234 | iFile:write(dumpTable(tab, "", title)) | ||
235 | iFile:close() | ||
236 | return true | ||
237 | end | ||
238 | |||
239 | -- TODO - should sanitise all these names. The same way SledjHamr does. | ||
240 | 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" | ||
241 | convertObject = function (source, dest, name) | ||
242 | -- It's a complex object, parse it's asset file and deal with it. | ||
243 | local tb = getXML(source) | ||
244 | if 0 == #tb then return end | ||
245 | local tbl = reduceXML(tb) | ||
246 | tbl.Name = name | ||
247 | tbl.AssetID = posix.basename(source):gsub("__" .. stripUUID, ""):gsub(".xml", "") | ||
248 | tbl.AssetType = 6 | ||
249 | |||
250 | local ddest = deDup(dest .. "/" .. name) | ||
251 | posix.mkdir(ddest) | ||
252 | if not writeTable(tbl, deDup(dest .. "/." .. name, "lua"), tbl.AssetID) then return end | ||
253 | |||
254 | if (nil ~= tbl.SceneObjectGroup) then | ||
255 | if (nil ~= tbl.SceneObjectGroup.RootPart) | ||
256 | and (nil ~= tbl.SceneObjectGroup.RootPart.SceneObjectPart) | ||
257 | and (nil ~= tbl.SceneObjectGroup.RootPart.SceneObjectPart.TaskInventory) then | ||
258 | for k, v in pairs(tbl.SceneObjectGroup.RootPart.SceneObjectPart.TaskInventory) do | ||
259 | if "TaskInventoryItem" == k:sub(1, 17) then | ||
260 | local ext = assetTypes[tonumber(v.Type)] | ||
261 | local fl = assetsDir .. "/" .. v.AssetID.UUID .. "_" .. ext | ||
262 | -- If it's an object.xml, parse it recursively. | ||
263 | if "object.xml" ~= ext then | ||
264 | os.execute('cp --backup=numbered "' .. fl .. '" "' .. deDup(ddest .. "/" .. sanitise(v.Name), ext) .. '"') | ||
265 | else | ||
266 | convertObject(fl, ddest, sanitise(v.Name)) | ||
267 | end | ||
268 | end | ||
269 | end | ||
270 | end | ||
271 | |||
272 | if (nil ~= tbl.SceneObjectGroup.OtherParts) then | ||
273 | for j, u in pairs(tbl.SceneObjectGroup.OtherParts) do | ||
274 | if "Part" == j:sub(1, 4) then | ||
275 | if (nil ~= u.SceneObjectPart) and (nil ~= u.SceneObjectPart.TaskInventory) then | ||
276 | local udest = ddest .. '/' .. sanitise(u.SceneObjectPart.Name) | ||
277 | posix.mkdir(udest) | ||
278 | for k, v in pairs(u.SceneObjectPart.TaskInventory) do | ||
279 | if "TaskInventoryItem" == k:sub(1, 17) then | ||
280 | local ext = assetTypes[tonumber(v.Type)] | ||
281 | local fl = assetsDir .. "/" .. v.AssetID.UUID .. "_" .. ext | ||
282 | -- If it's an object.xml, parse it recursively. | ||
283 | if "object.xml" ~= ext then | ||
284 | os.execute('cp --backup=numbered "' .. fl .. '" "' .. deDup(udest .. '/' .. sanitise(v.Name), ext) .. '"') | ||
285 | else | ||
286 | convertObject(fl, ddest, sanatise(v.Name)) | ||
287 | end | ||
288 | end | ||
289 | end | ||
290 | end | ||
291 | end | ||
292 | end | ||
293 | end | ||
294 | end | ||
295 | end | ||
296 | |||
297 | convertFiles = function (st, source) | ||
298 | local name = posix.basename(source):gsub("__" .. stripUUID, ""):gsub(".xml", "") | ||
299 | local dest = "IARs/" .. posix.dirname( source):gsub("__" .. stripUUID, "") | ||
300 | local object = {} | ||
301 | |||
302 | -- Sanitise the directory name. | ||
303 | local d = dest | ||
304 | dest = "" | ||
305 | while "." ~= d do | ||
306 | dest = sanitise(posix.basename(d)) .. "/" .. dest | ||
307 | d = posix.dirname(d) | ||
308 | end | ||
309 | dest = dest:sub(1, -2) | ||
310 | |||
311 | posix.mkdir(dest) | ||
312 | if "regular" ~= st then return end | ||
313 | |||
314 | -- Parse the IAR XML file. which we assume this is. | ||
315 | for k, v in pairs(getXML(source)) do | ||
316 | if nil ~= v.tag then object[v.tag] = v[1] end | ||
317 | end | ||
318 | |||
319 | -- Double check we got an asset ID. | ||
320 | if nil ~= object.AssetID then | ||
321 | -- In SledjHamr these .lua table files are called .omg, and have a different structure. Call these ones .liar / .loar? .lOS? | ||
322 | local tp = assetTypes[tonumber(object.AssetType)] | ||
323 | if nil == tp then tp = "UNKNOWN" end | ||
324 | local asset = assetsDir .. "/" .. object.AssetID .. "_" .. tp | ||
325 | |||
326 | if "object.xml" ~= tp then | ||
327 | -- If it's an ordinary asset, create the metadata file and copy the asset file. | ||
328 | writeTable(object, deDup(dest .. "/." .. sanitise(name), "lua"), object.AssetID) | ||
329 | os.execute('cp --backup=numbered "' .. asset .. '" "' .. deDup(dest .. "/" .. sanitise(name), tp) .. '"') | ||
330 | else | ||
331 | convertObject(asset, dest, sanitise(object.Name)) | ||
332 | end | ||
333 | |||
334 | end | ||
335 | end | ||
336 | |||
337 | scanDir = function (dir, func) | ||
338 | local files, errstr, errno = posix.dir(dir) | ||
339 | if files then | ||
340 | for a,b in ipairs(files) do | ||
341 | if ("." ~= b) and (".." ~= b) then | ||
342 | local file = dir .. "/" .. b | ||
343 | local st = posix.stat(file, "type") | ||
344 | if nil ~= st then | ||
345 | if nil ~= func then func(st, file) end | ||
346 | if "directory" == st then scanDir(dir .. "/" .. b, func) end | ||
347 | else | ||
348 | print("NOT" .. " " .. file) | ||
349 | end | ||
350 | end | ||
351 | end | ||
352 | else | ||
353 | print(errstr) | ||
354 | end | ||
355 | end | ||
356 | |||
357 | |||
358 | if 0 ~= #args then | ||
359 | end | ||
360 | |||
361 | |||
362 | local e = tarball:gsub("(.+)%.(%a+)", "%2") | ||
363 | local archiver = "z" | ||
364 | local IARname = tarball:gsub("(.+)%.(%a+)", "%1") | ||
365 | if ("iar" ~= e:lower()) then | ||
366 | archiver = "a" | ||
367 | -- Assume the file is a tarball with ".tar." as part of the name. | ||
368 | IARname = IARname:gsub("(.+)%.(%a+)", "%1") | ||
369 | end | ||
370 | assetsDir = IARname .. "/assets" | ||
371 | os.execute("rm -rf " .. IARname) | ||
372 | posix.mkdir(IARname) | ||
373 | os.execute("tar -x" .. archiver .. "f " .. tarball .. " -C " .. IARname) | ||
374 | |||
375 | |||
376 | posix.mkdir("IARs") | ||
377 | os.execute("rm -rf IARs/" .. IARname) | ||
378 | posix.mkdir("IARs/" .. IARname) | ||
379 | posix.mkdir("IARs/" .. IARname .."/inventory") | ||
380 | |||
381 | local inv = IARname .. "/inventory" | ||
382 | local st = posix.stat(inv, "type") | ||
383 | if nil == st then | ||
384 | assetsDir = IARname .. "/" .. assetsDir | ||
385 | inv = IARname .. "/" .. inv | ||
386 | posix.mkdir("IARs/" .. IARname) | ||
387 | posix.mkdir("IARs/" .. IARname .."/" .. IARname) | ||
388 | posix.mkdir("IARs/" .. IARname .."/" .. IARname .. "/inventory") | ||
389 | end | ||
390 | |||
391 | scanDir(inv, convertFiles) | ||
392 | --os.execute("rm -rf " .. IARname) | ||