|
1 |
| -const CHARSET = new Uint8Array([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 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, 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, 46, 45, 58, 43, 61, 94, 33, 47, 42, 63, 38, 60, 62, 40, 41, 91, 93, 123, 125, 64, 37, 36, 35]); |
2 |
| -const POW_85 = [1, 85, 7225, 614125, 52200625]; |
3 |
| -/** @type {Uint8Array} */ |
4 |
| -let REVERSE_MAP; |
| 1 | +const ENCODE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#"; |
| 2 | +const DECODE = [-1, 68, -1, 84, 83, 82, 72, -1, 75, 76, 70, 65, -1, 63, 62, 69, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 64, -1, 73, 66, 74, 71, 81, 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, 77, -1, 78, 67, -1, -1, 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, 79, -1, 80, -1, -1]; |
| 3 | +const POW_85 = [0, 1, 85, 7225, 614125, 52200625]; |
| 4 | +const POW_256 = [1, 256, 65536, 16777216]; |
5 | 5 |
|
6 | 6 | /**
|
7 | 7 | * Encodes binary data into a Z85 string.
|
8 | 8 | * @param {ArrayBuffer} data - The binary data to encode
|
9 | 9 | * @returns {string} The Z85 encoded string
|
10 | 10 | */
|
11 |
| -export function encode85(data) { |
12 |
| - const byteLength = data.byteLength; |
13 |
| - const fullBlocks = Math.floor(byteLength / 4); |
14 |
| - const remain = byteLength % 4; |
15 |
| - const outputLength = Math.ceil(byteLength * 5 / 4); |
16 |
| - const target = new Uint8Array(outputLength); |
| 11 | +function encode85(data) { |
17 | 12 | const dv = new DataView(data);
|
18 |
| - |
19 |
| - let targetIndex = 0; |
20 |
| - |
21 |
| - // Process complete 4-byte blocks |
22 |
| - for (let i = 0; i < fullBlocks; i++) { |
23 |
| - let value = dv.getUint32(i * 4); |
24 |
| - |
25 |
| - // Encode 5 characters |
26 |
| - target[targetIndex + 4] = CHARSET[value % 85]; |
27 |
| - value = Math.floor(value / 85); |
28 |
| - target[targetIndex + 3] = CHARSET[value % 85]; |
29 |
| - value = Math.floor(value / 85); |
30 |
| - target[targetIndex + 2] = CHARSET[value % 85]; |
31 |
| - value = Math.floor(value / 85); |
32 |
| - target[targetIndex + 1] = CHARSET[value % 85]; |
33 |
| - target[targetIndex] = CHARSET[Math.floor(value / 85)]; |
34 |
| - |
35 |
| - targetIndex += 5; |
36 |
| - } |
37 |
| - |
38 |
| - // Handle remaining bytes |
39 |
| - if (remain) { |
40 |
| - let value = 0; |
41 |
| - for (let i = 0; i < remain; i++) { |
42 |
| - value = (value << 8) | dv.getUint8(fullBlocks * 4 + i); |
43 |
| - } |
44 |
| - value <<= (4 - remain) * 8; // Pad remaining bytes |
45 |
| - |
46 |
| - const partialOutput = []; |
47 |
| - for (let i = 0; i < Math.ceil((remain + 1) * 5 / 4); i++) { |
48 |
| - partialOutput.unshift(CHARSET[value % 85]); |
49 |
| - value = Math.floor(value / 85); |
50 |
| - } |
51 |
| - |
52 |
| - for (const char of partialOutput) { |
53 |
| - target[targetIndex++] = char; |
| 13 | + const length = dv.byteLength; |
| 14 | + const padding = (4 - (length % 4)) % 4; |
| 15 | + |
| 16 | + let result = '', value = 0; |
| 17 | + for (let i = 0; i < length + padding; ++i) { |
| 18 | + const isPadding = i >= length; |
| 19 | + value = value * 256 + (isPadding ? 0 : dv.getUint8(i)); |
| 20 | + if ((i + 1) % 4 === 0) { |
| 21 | + for (let j = 5; j > 0; --j) { |
| 22 | + if (isPadding && j <= padding) |
| 23 | + continue; |
| 24 | + |
| 25 | + result += ENCODE[Math.floor(value / POW_85[j]) % 85]; |
| 26 | + } |
| 27 | + value = 0; |
54 | 28 | }
|
55 | 29 | }
|
56 | 30 |
|
57 |
| - return new TextDecoder().decode(target); |
58 |
| -} |
59 |
| - |
60 |
| -/** |
61 |
| - * Creates a reverse mapping from character codes to indices. |
62 |
| - * @param {Uint8Array} mapOrig - The original character map |
63 |
| - * @returns {Uint8Array} A mapping of character codes to indices |
64 |
| - * @private |
65 |
| - */ |
66 |
| -function getReverseMap(mapOrig) { |
67 |
| - const revMap = new Uint8Array(128); |
68 |
| - for (const [num, charCode] of Object.entries(mapOrig)) { |
69 |
| - revMap[charCode] = parseInt(num); |
70 |
| - } |
71 |
| - return revMap; |
72 |
| -} |
| 31 | + return result; |
| 32 | +}; |
73 | 33 |
|
74 | 34 | /**
|
75 | 35 | * Decodes a Z85 string into binary data.
|
76 | 36 | * @param {string} string - The Z85 encoded string
|
77 | 37 | * @returns {ArrayBuffer} The decoded binary data
|
78 | 38 | */
|
79 |
| -export function decode85(string) { |
80 |
| - if (!REVERSE_MAP) REVERSE_MAP = getReverseMap(CHARSET); |
81 |
| - const z85ab = new Uint8Array(string.length); |
82 |
| - for (let i = 0; i < string.length; i++) { |
83 |
| - z85ab[i] = string.charCodeAt(i); |
84 |
| - } |
85 |
| - |
86 |
| - const pad = (5 - (z85ab.length % 5)) % 5; |
87 |
| - const result = new Uint8Array((Math.ceil(z85ab.length / 5) * 4) - pad); |
88 |
| - const dv = new DataView(result.buffer); |
89 |
| - |
90 |
| - // Process complete 5-character blocks |
91 |
| - const completeBlocks = Math.floor(z85ab.length / 5) - 1; |
92 |
| - for (let i = 0; i <= completeBlocks; i++) { |
93 |
| - const chunk = z85ab.slice(i * 5, i * 5 + 5); |
94 |
| - const value = chunk.reduceRight((acc, char, idx) => { |
95 |
| - return acc + REVERSE_MAP[char] * POW_85[4 - idx]; |
96 |
| - }, 0); |
97 |
| - dv.setUint32(i * 4, value); |
98 |
| - } |
99 |
| - |
100 |
| - // Handle remaining characters |
101 |
| - if (pad > 0) { |
102 |
| - const lastIndex = completeBlocks + 1; |
103 |
| - const lastChar = CHARSET[CHARSET.length - 1]; |
104 |
| - const lastPart = new Uint8Array([ |
105 |
| - ...z85ab.slice(lastIndex * 5), |
106 |
| - lastChar, lastChar, lastChar, lastChar |
107 |
| - ]); |
108 |
| - |
109 |
| - const value = [...lastPart].slice(0, 5).reduceRight((acc, char, idx) => { |
110 |
| - return acc + REVERSE_MAP[char] * POW_85[4 - idx]; |
111 |
| - }, 0); |
112 |
| - |
113 |
| - const lastDv = new DataView(lastPart.buffer); |
114 |
| - lastDv.setUint32(0, value); |
115 |
| - |
116 |
| - const remainingBytes = 4 - pad; |
117 |
| - for (let j = 0; j < remainingBytes; j++) { |
118 |
| - result[lastIndex * 4 + j] = lastPart[j]; |
119 |
| - } |
| 39 | +function decode85(string) { |
| 40 | + const remainder = string.length % 5; |
| 41 | + const padding = 5 - (remainder === 0 ? 5 : remainder); |
| 42 | + string = string.padEnd(string.length + padding, ENCODE[ENCODE.length - 1]); |
| 43 | + const length = string.length; |
| 44 | + |
| 45 | + let buffer = new Uint8Array((length * 4 / 5) - padding); |
| 46 | + let value = 0, char = 0, byte = 0; |
| 47 | + for (let i = 0; i < length; ++i) { |
| 48 | + value = value * 85 + DECODE[string.charCodeAt(char++) - 32]; |
| 49 | + if (char % 5 !== 0) continue; |
| 50 | + |
| 51 | + for (let j = 3; j >= 0; --j) |
| 52 | + buffer[byte++] = Math.floor(value / POW_256[j]) % 256; |
| 53 | + value = 0; |
120 | 54 | }
|
121 | 55 |
|
122 |
| - return result.buffer; |
| 56 | + return buffer.buffer; |
123 | 57 | }
|
0 commit comments