|
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