Repository navigation
Expand file tree
/
Copy pathserialize.lua
More file actions
264 lines (228 loc) · 7.45 KB
/
Copy pathserialize.lua
File metadata and controls
264 lines (228 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
local _, ns = ...
local base64 = ns.base64
local crc32 = ns.crc32
-- Max format version this build reads. Full profiles still ENCODE as v2 so
-- earlier addon revisions can decode them; v3 (captured-bars list) is used
-- only for partial profiles, which older revisions couldn't restore correctly
-- anyway.
local VERSION = 3
-- ── Packer ────────────────────────────────────────────────────────────────────
local function Pack(profile)
local buf = {}
local function byte(n) buf[#buf+1] = n % 256 end
local function int16(n)
buf[#buf+1] = math.floor(n / 256) % 256
buf[#buf+1] = n % 256
end
local function int24(n)
buf[#buf+1] = math.floor(n / 65536) % 256
buf[#buf+1] = math.floor(n / 256) % 256
buf[#buf+1] = n % 256
end
local function str8(s)
s = s or ""
-- The length must fit one byte; wrapping it (#s % 256) would silently
-- corrupt the whole export from this field on.
assert(#s <= 255, "profile string too long to export (" .. #s .. " > 255): " .. string.sub(s, 1, 40))
byte(#s)
for i = 1, #s do buf[#buf+1] = string.byte(s, i) end
end
local function str16(s)
s = s or ""
int16(#s)
for i = 1, #s do buf[#buf+1] = string.byte(s, i) end
end
-- metadata
str8(profile.char or "")
str8(profile.class or "")
str8(profile.spec or "")
-- slots
local slots = profile.slots or {}
int16(#slots)
for _, s in ipairs(slots) do
byte(s.id)
str8(s.type)
int24(s.index or 0)
str8(s.strindex or "")
byte(s.profSlot or 0)
end
-- bindings
local binds = profile.binds or {}
int16(#binds)
for _, b in ipairs(binds) do
str8(b.command)
str8(b.key1 or "")
str8(b.key2 or "")
end
-- macros
local macros = profile.macros or {}
int16(#macros)
for _, m in ipairs(macros) do
int16(m.id)
str8(m.name)
str8(m.icon)
str16(m.body or "")
end
-- pet slots
local petslots = profile.petslots or {}
byte(#petslots)
for _, p in ipairs(petslots) do
byte(p.id)
str8(p.type)
int24(p.index or 0)
str8(p.strindex or "")
end
-- outfits
local outfits = profile.outfits or {}
byte(#outfits)
for _, name in ipairs(outfits) do
str8(name)
end
-- captured-bars list (v3+; present only for partial profiles)
if profile.bars then
byte(#profile.bars)
for _, b in ipairs(profile.bars) do byte(b) end
end
return buf
end
-- ── Unpacker ──────────────────────────────────────────────────────────────────
local function Unpack(buf, ver)
local pos = 1
local function byte()
local v = buf[pos]; pos = pos + 1; return v
end
local function int16()
local hi, lo = buf[pos], buf[pos+1]; pos = pos + 2
return hi * 256 + lo
end
local function int24()
local a, b, c = buf[pos], buf[pos+1], buf[pos+2]; pos = pos + 3
return a * 65536 + b * 256 + c
end
local function str8()
local len = byte()
local s = {}
for _ = 1, len do s[#s+1] = string.char(buf[pos]); pos = pos + 1 end
return table.concat(s)
end
local function str16()
local len = int16()
local s = {}
for _ = 1, len do s[#s+1] = string.char(buf[pos]); pos = pos + 1 end
return table.concat(s)
end
local profile = {}
profile.char = str8()
profile.class = str8()
profile.spec = str8()
local nSlots = int16()
profile.slots = {}
for _ = 1, nSlots do
local s = { id = byte(), type = str8(), index = int24(), strindex = str8() }
if ver >= 2 then
local ps = byte()
if ps > 0 then s.profSlot = ps end
end
if s.index == 0 then s.index = nil end
if s.strindex == "" then s.strindex = nil end
profile.slots[#profile.slots+1] = s
end
local nBinds = int16()
profile.binds = {}
for _ = 1, nBinds do
local b = { command = str8(), key1 = str8(), key2 = str8() }
if b.key1 == "" then b.key1 = nil end
if b.key2 == "" then b.key2 = nil end
profile.binds[#profile.binds+1] = b
end
local nMacros = int16()
profile.macros = {}
for _ = 1, nMacros do
profile.macros[#profile.macros+1] = {
id = int16(), name = str8(), icon = str8(), body = str16(),
}
end
local nPet = byte()
profile.petslots = {}
for _ = 1, nPet do
local p = { id = byte(), type = str8(), index = int24(), strindex = str8() }
if p.index == 0 then p.index = nil end
if p.strindex == "" then p.strindex = nil end
profile.petslots[#profile.petslots+1] = p
end
local nOutfits = byte()
profile.outfits = {}
for _ = 1, nOutfits do
profile.outfits[#profile.outfits+1] = str8()
end
if ver >= 3 then
local nBars = byte()
if nBars and nBars > 0 then
profile.bars = {}
for _ = 1, nBars do profile.bars[#profile.bars+1] = byte() end
end
end
return profile
end
-- ── Public API ────────────────────────────────────────────────────────────────
local LINE_LEN = 60
local SEP = IsWindowsClient() and "\r\n" or "\n"
---Encode a profile table to a copyable text string.
---@param profile table
---@return string
function ns.Encode(profile)
local payload = Pack(profile)
-- header: [version(1)] [crc(4)] [payload...]
local ver = profile.bars and 3 or 2
local frame = { ver, 0, 0, 0, 0 }
for _, b in ipairs(payload) do frame[#frame+1] = b end
local crc = crc32.enc(frame)
-- store crc using integer bit ops to stay in integer domain
frame[2] = bit.band(bit.rshift(crc, 24), 0xFF)
frame[3] = bit.band(bit.rshift(crc, 16), 0xFF)
frame[4] = bit.band(bit.rshift(crc, 8), 0xFF)
frame[5] = bit.band(crc, 0xFF)
local b64 = base64.enc(frame)
local lines = {
"# Action Bar Master",
"# " .. (profile.char or "") .. " / " .. (profile.class or "") .. " / " .. (profile.spec or ""),
"# --------------------",
}
for i = 1, #b64, LINE_LEN do
lines[#lines+1] = b64:sub(i, i + LINE_LEN - 1)
end
lines[#lines+1] = "# --------------------"
return table.concat(lines, SEP)
end
---Decode a text string back to a profile table. Returns profile or nil, errmsg.
---@param text string
---@return table|nil, string|nil
function ns.Decode(text)
-- strip comment lines and whitespace
local s = text:gsub("(#[^\n]*\n?)", ""):gsub("[@][^\n]*\n?", "")
s = s:gsub("[\r\n%s]", "")
local frame = base64.dec(s)
if #frame < 5 then return nil, "Text too short" end
local ver = frame[1]
if ver < 1 or ver > VERSION then return nil, "Unsupported version: " .. ver end
-- verify crc
local stored = bit.bor(
bit.lshift(frame[2], 24),
bit.lshift(frame[3], 16),
bit.lshift(frame[4], 8),
frame[5]
)
local saved2, saved3, saved4, saved5 = frame[2], frame[3], frame[4], frame[5]
frame[2], frame[3], frame[4], frame[5] = 0, 0, 0, 0
local computed = crc32.enc(frame)
frame[2], frame[3], frame[4], frame[5] = saved2, saved3, saved4, saved5
if stored ~= bit.band(computed, 0xFFFFFFFF) then
return nil, "CRC mismatch — text may be corrupted"
end
-- unpack payload (skip 5-byte header)
local payload = {}
for i = 6, #frame do payload[#payload+1] = frame[i] end
local ok, result = pcall(Unpack, payload, ver)
if not ok then return nil, "Decode error: " .. tostring(result) end
return result
end