bplist-lossless 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,536 @@
1
+ import { PlistDate } from '../classes/plist-date.js';
2
+ import { UID } from '../classes/uid.js';
3
+ import { Utf16String } from '../classes/utf16-string.js';
4
+
5
+ export function serializeBplist(dicts: unknown) {
6
+ var buffer = new WritableStreamBuffer();
7
+ buffer.write(Buffer.from("bplist00"));
8
+
9
+ var entries = toEntries(dicts);
10
+ var idSizeInBytes = computeIdSizeInBytes(entries.length);
11
+ var offsets: number[] = [];
12
+ var offsetSizeInBytes: number;
13
+ var offsetTableOffset: number;
14
+
15
+ updateEntryIds();
16
+
17
+ entries.forEach(function(entry, entryIdx) {
18
+ offsets[entryIdx] = buffer.size();
19
+ if (!entry) {
20
+ buffer.write(0x00);
21
+ } else {
22
+ write(entry);
23
+ }
24
+ });
25
+
26
+ writeOffsetTable();
27
+ writeTrailer();
28
+ return buffer.getContents();
29
+
30
+ function updateEntryIds() {
31
+ var strings: Record<string, any> = {};
32
+ var entryId = 0;
33
+ entries.forEach(function(entry) {
34
+ if (entry.id) {
35
+ return;
36
+ }
37
+ if (entry.type === 'string') {
38
+ if (!entry.bplistOverride && strings.hasOwnProperty(entry.value)) {
39
+ entry.type = 'stringref';
40
+ entry.id = strings[entry.value];
41
+ } else {
42
+ strings[entry.value] = entry.id = entryId++;
43
+ }
44
+ } else {
45
+ entry.id = entryId++;
46
+ }
47
+ });
48
+
49
+ entries = entries.filter(function(entry) {
50
+ return (entry.type !== 'stringref');
51
+ });
52
+ }
53
+
54
+ function writeTrailer() {
55
+ // 6 null bytes
56
+ buffer.write(Buffer.from([0, 0, 0, 0, 0, 0]));
57
+
58
+ // size of an offset
59
+ writeByte(offsetSizeInBytes);
60
+
61
+ // size of a ref
62
+ writeByte(idSizeInBytes);
63
+
64
+ // number of objects
65
+ writeLong(entries.length);
66
+
67
+ // top object
68
+ writeLong(0);
69
+
70
+ // offset table offset
71
+ writeLong(offsetTableOffset);
72
+ }
73
+
74
+ function writeOffsetTable() {
75
+ offsetTableOffset = buffer.size();
76
+ offsetSizeInBytes = computeOffsetSizeInBytes(offsetTableOffset);
77
+ offsets.forEach(function(offset) {
78
+ writeBytes(offset, offsetSizeInBytes);
79
+ });
80
+ }
81
+
82
+ function write(entry: any) {
83
+ switch (entry.type) {
84
+ case 'dict':
85
+ writeDict(entry);
86
+ break;
87
+ case 'number':
88
+ case 'double':
89
+ writeNumber(entry);
90
+ break;
91
+ case 'UID':
92
+ writeUID(entry);
93
+ break;
94
+ case 'array':
95
+ writeArray(entry);
96
+ break;
97
+ case 'boolean':
98
+ writeBoolean(entry);
99
+ break;
100
+ case 'string':
101
+ case 'string-utf16':
102
+ writeString(entry);
103
+ break;
104
+ case 'date':
105
+ writeDate(entry);
106
+ break;
107
+ case 'data':
108
+ writeData(entry);
109
+ break;
110
+ case 'null':
111
+ writeNull()
112
+ break;
113
+ default:
114
+ throw new Error("unhandled entry type: " + entry.type);
115
+ }
116
+ }
117
+
118
+ function writeDate(entry: any) {
119
+ writeByte(0x33);
120
+
121
+ const raw = PlistDate.from(entry.value).toBuffer();
122
+ buffer.write(raw);
123
+ }
124
+
125
+ function writeDict(entry: any) {
126
+ writeIntHeader(0xD, entry.entryKeys.length);
127
+ entry.entryKeys.forEach(function(entry: any) {
128
+ writeID(entry.id);
129
+ });
130
+ entry.entryValues.forEach(function(entry: any) {
131
+ writeID(entry.id);
132
+ });
133
+ }
134
+
135
+ function writeNumber(entry: any) {
136
+ if (typeof entry.value === 'bigint') {
137
+ const size = getIntSize(entry.value);
138
+ const header = 0x10 | Math.log2(size);
139
+
140
+ writeByte(header);
141
+
142
+ const buf = bigintToBuffer(entry.value, size);
143
+ buffer.write(buf);
144
+ } else if (entry.type !== 'double' && parseFloat(entry.value).toFixed() == entry.value) {
145
+ if (entry.value < 0) {
146
+ writeByte(0x13);
147
+ writeBytes(entry.value, 8, true);
148
+ } else if (entry.value <= 0xff) {
149
+ writeByte(0x10);
150
+ writeBytes(entry.value, 1);
151
+ } else if (entry.value <= 0xffff) {
152
+ writeByte(0x11);
153
+ writeBytes(entry.value, 2);
154
+ } else if (entry.value <= 0xffffffff) {
155
+ writeByte(0x12);
156
+ writeBytes(entry.value, 4);
157
+ } else {
158
+ writeByte(0x13);
159
+ writeBytes(entry.value, 8);
160
+ }
161
+ } else {
162
+ writeByte(0x23);
163
+ writeDouble(entry.value);
164
+ }
165
+ }
166
+
167
+ function writeUID(entry: any) {
168
+ let raw: Buffer;
169
+
170
+ if (entry.value instanceof UID) {
171
+ raw = Buffer.from(
172
+ entry.value.buffer,
173
+ entry.value.byteOffset,
174
+ entry.value.byteLength,
175
+ );
176
+ } else if (typeof entry.value === "bigint") {
177
+ if (entry.value < 0n) {
178
+ throw new TypeError("UID must be unsigned");
179
+ }
180
+
181
+ let hex = entry.value.toString(16);
182
+ if (hex.length % 2 !== 0) hex = "0" + hex;
183
+ raw = hex.length === 0 ? Buffer.from([0]) : Buffer.from(hex || "00", "hex");
184
+ } else if (
185
+ typeof entry.value === "number" &&
186
+ Number.isInteger(entry.value) &&
187
+ entry.value >= 0
188
+ ) {
189
+ let n = BigInt(entry.value);
190
+ let hex = n.toString(16);
191
+ if (hex.length % 2 !== 0) hex = "0" + hex;
192
+ raw = Buffer.from(hex || "00", "hex");
193
+ } else {
194
+ throw new TypeError("UID value must be a UID, bigint, or unsigned integer number");
195
+ }
196
+
197
+ // Canonical: strip leading zero bytes, but keep at least one byte.
198
+ let start = 0;
199
+ while (start < raw.length - 1 && raw[start] === 0) start++;
200
+ raw = raw.subarray(start);
201
+
202
+ if (raw.length < 1 || raw.length > 16) {
203
+ throw new RangeError(`UID must be between 1 and 16 bytes, got ${raw.length}`);
204
+ }
205
+
206
+ writeByte(0x80 | (raw.length - 1));
207
+ buffer.write(raw);
208
+ }
209
+
210
+ function writeArray(entry: any) {
211
+ writeIntHeader(0xA, entry.entries.length);
212
+ entry.entries.forEach(function(e: any) {
213
+ writeID(e.id);
214
+ });
215
+ }
216
+
217
+ function writeBoolean(entry: any) {
218
+ writeByte(entry.value ? 0x09 : 0x08);
219
+ }
220
+
221
+ function writeNull() {
222
+ writeByte(0x00);
223
+ }
224
+
225
+
226
+ function writeString(entry: any) {
227
+ if (entry.type === 'string-utf16') {
228
+ let utf16: Buffer;
229
+
230
+ if (Utf16String.isUtf16String(entry.value)) {
231
+ // ✅ USE RAW BYTES DIRECTLY
232
+ utf16 = Buffer.from(
233
+ entry.value.buffer,
234
+ entry.value.byteOffset,
235
+ entry.value.byteLength
236
+ );
237
+ } else {
238
+ // string → UTF-16LE → convert to BE
239
+ const le = Buffer.from(entry.value, 'ucs2');
240
+ utf16 = Buffer.alloc(le.length);
241
+
242
+ for (let i = 0; i < le.length; i += 2) {
243
+ utf16[i] = le[i + 1]!;
244
+ utf16[i + 1] = le[i]!;
245
+ }
246
+ }
247
+
248
+ writeIntHeader(0x6, utf16.length / 2);
249
+ buffer.write(utf16);
250
+ } else {
251
+ const ascii = Buffer.from(entry.value, 'latin1');
252
+ writeIntHeader(0x5, ascii.length);
253
+ buffer.write(ascii);
254
+ }
255
+ }
256
+
257
+ function writeData(entry: any) {
258
+ writeIntHeader(0x4, entry.value.length);
259
+ buffer.write(entry.value);
260
+ }
261
+
262
+ function writeLong(l: number) {
263
+ writeBytes(l, 8);
264
+ }
265
+
266
+ function writeByte(b: number) {
267
+ buffer.write(Buffer.from([b]));
268
+ }
269
+
270
+ function writeDouble(v: number) {
271
+ var buf = Buffer.alloc(8);
272
+ buf.writeDoubleBE(v, 0);
273
+ buffer.write(buf);
274
+ }
275
+
276
+ function writeIntHeader(kind: number, value: number) {
277
+ if (value < 15) {
278
+ writeByte((kind << 4) + value);
279
+ } else if (value < 256) {
280
+ writeByte((kind << 4) + 15);
281
+ writeByte(0x10);
282
+ writeBytes(value, 1);
283
+ } else if (value < 65536) {
284
+ writeByte((kind << 4) + 15);
285
+ writeByte(0x11);
286
+ writeBytes(value, 2);
287
+ } else {
288
+ writeByte((kind << 4) + 15);
289
+ writeByte(0x12);
290
+ writeBytes(value, 4);
291
+ }
292
+ }
293
+
294
+ function writeID(id: number) {
295
+ writeBytes(id, idSizeInBytes);
296
+ }
297
+
298
+ function writeBytes(value: number, bytes: number, is_signedint = false) {
299
+ // write low-order bytes big-endian style
300
+ var buf = Buffer.alloc(bytes);
301
+ var z = 0;
302
+
303
+ // javascript doesn't handle large numbers
304
+ while (bytes > 4) {
305
+ buf[z++] = is_signedint ? 0xff : 0;
306
+ bytes--;
307
+ }
308
+
309
+ for (var i = bytes - 1; i >= 0; i--) {
310
+ buf[z++] = value >> (8 * i);
311
+ }
312
+ buffer.write(buf);
313
+ }
314
+ };
315
+
316
+ function toEntries(dicts: any) {
317
+ if (dicts === null) {
318
+ return [
319
+ {
320
+ type: 'null',
321
+ value: null
322
+ }
323
+ ];
324
+ } else if (typeof dicts === 'boolean') {
325
+ return [
326
+ {
327
+ type: 'boolean',
328
+ value: dicts
329
+ }
330
+ ];
331
+ } else if (typeof dicts === 'bigint') {
332
+ return [
333
+ {
334
+ type: 'number',
335
+ value: dicts
336
+ }
337
+ ];
338
+ } else if (typeof dicts === 'number') {
339
+ return [
340
+ {
341
+ type: 'double',
342
+ value: dicts
343
+ }
344
+ ];
345
+ } else if (typeof dicts === 'string') {
346
+ return [
347
+ {
348
+ type: mustBeUtf16(dicts) ? 'string-utf16' : 'string',
349
+ value: dicts
350
+ }
351
+ ];
352
+ } else if (Utf16String.isUtf16String(dicts)) {
353
+ return [
354
+ {
355
+ type: 'string-utf16',
356
+ value: dicts
357
+ }
358
+ ]
359
+ } else if (UID.isUID(dicts)) {
360
+ return [
361
+ {
362
+ type: 'UID',
363
+ value: dicts
364
+ }
365
+ ]
366
+ } else if (Buffer.isBuffer((dicts))) {
367
+ return [
368
+ {
369
+ type: 'data',
370
+ value: dicts
371
+ }
372
+ ];
373
+ } else if (
374
+ PlistDate.isPlistDate(dicts) ||
375
+ Object.prototype.toString.call(dicts) === '[object Date]'
376
+ ) {
377
+ return [
378
+ {
379
+ type: 'date',
380
+ value: dicts
381
+ }
382
+ ]
383
+ } else if (Array.isArray(dicts)) {
384
+ return toEntriesArray(dicts);
385
+ } else if (isPlainObject(dicts)) {
386
+ return toEntriesObject(dicts);
387
+ } else {
388
+ throw new Error('unhandled entry: ' + dicts);
389
+ }
390
+ }
391
+
392
+ function toEntriesArray(arr: unknown[]) {
393
+ var results = [
394
+ {
395
+ type: 'array',
396
+ entries: [] as unknown[]
397
+ }
398
+ ];
399
+ arr.forEach(function(v) {
400
+ var entry = toEntries(v);
401
+ results[0]!.entries.push(entry[0]);
402
+ results = results.concat(entry);
403
+ });
404
+ return results;
405
+ }
406
+
407
+ function toEntriesObject(dict: Record<string, unknown>) {
408
+ const result = {
409
+ type: 'dict',
410
+ entryKeys: [] as unknown[],
411
+ entryValues: [] as unknown[],
412
+ };
413
+
414
+ const results: any[] = [result];
415
+
416
+ for (const key of Reflect.ownKeys(dict)) {
417
+ if (typeof key !== "string") continue;
418
+
419
+ const entryKey = toEntries(key);
420
+ const entryValue = toEntries((dict as any)[key]);
421
+
422
+ result.entryKeys.push(entryKey[0]!);
423
+ result.entryValues.push(entryValue[0]!);
424
+
425
+ results.push(...entryKey);
426
+ results.push(...entryValue);
427
+ }
428
+
429
+ return results;
430
+ }
431
+
432
+ function computeOffsetSizeInBytes(maxOffset: number) {
433
+ if (maxOffset < 256) {
434
+ return 1;
435
+ }
436
+ if (maxOffset < 65536) {
437
+ return 2;
438
+ }
439
+ if (maxOffset < 4294967296) {
440
+ return 4;
441
+ }
442
+ return 8;
443
+ }
444
+
445
+ function computeIdSizeInBytes(numberOfIds: number) {
446
+ if (numberOfIds < 256) {
447
+ return 1;
448
+ }
449
+ if (numberOfIds < 65536) {
450
+ return 2;
451
+ }
452
+ return 4;
453
+ }
454
+
455
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
456
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
457
+ return false;
458
+ }
459
+
460
+ const proto = Object.getPrototypeOf(value);
461
+ return proto === null || proto === Object.prototype;
462
+ }
463
+
464
+ class WritableStreamBuffer {
465
+ _chunks: Buffer[];
466
+ _size: number;
467
+
468
+ constructor() {
469
+ this._chunks = [];
470
+ this._size = 0;
471
+ }
472
+
473
+ write(chunk: unknown, encoding?: any) {
474
+ let buf;
475
+
476
+ if (typeof chunk === 'number') {
477
+ buf = Buffer.from([chunk & 0xff]);
478
+ } else if (Buffer.isBuffer(chunk)) {
479
+ // copy to mirror stream-buffers behavior more closely
480
+ buf = Buffer.from(chunk);
481
+ } else if (chunk instanceof Uint8Array) {
482
+ buf = Buffer.from(chunk);
483
+ } else if (typeof chunk === 'string') {
484
+ buf = Buffer.from(chunk, encoding);
485
+ } else {
486
+ throw new TypeError('Unsupported chunk type passed to write()');
487
+ }
488
+
489
+ this._chunks.push(buf);
490
+ this._size += buf.length;
491
+ return true;
492
+ }
493
+
494
+ size() {
495
+ return this._size;
496
+ }
497
+
498
+ getContents() {
499
+ return Buffer.concat(this._chunks, this._size);
500
+ }
501
+ }
502
+
503
+
504
+ function bigintToBuffer(value: bigint, size: number): Buffer {
505
+ const buf = Buffer.alloc(size);
506
+
507
+ let temp = value;
508
+
509
+ // handle negative via two's complement
510
+ if (value < 0) {
511
+ temp = (1n << BigInt(size * 8)) + value;
512
+ }
513
+
514
+ for (let i = size - 1; i >= 0; i--) {
515
+ buf[i] = Number(temp & 0xffn);
516
+ temp >>= 8n;
517
+ }
518
+
519
+ return buf;
520
+ }
521
+
522
+
523
+ function getIntSize(value: bigint): number {
524
+ if (value >= -0x80n && value <= 0x7fn) return 1;
525
+ if (value >= -0x8000n && value <= 0x7fffn) return 2;
526
+ if (value >= -0x80000000n && value <= 0x7fffffffn) return 4;
527
+ if (value >= -0x8000000000000000n && value <= 0x7fffffffffffffffn) return 8;
528
+ return 16;
529
+ }
530
+
531
+ function mustBeUtf16(str: string) {
532
+ for (let i = 0; i < str.length; i++) {
533
+ if (str.charCodeAt(i) > 0x7f) return true;
534
+ }
535
+ return false;
536
+ }