Skip to content

Commit 0873b01

Browse files
committed
switch from base64 to z85
1 parent f3fd2aa commit 0873b01

File tree

5 files changed

+152
-146
lines changed

5 files changed

+152
-146
lines changed

src/base64.js

Lines changed: 0 additions & 110 deletions
This file was deleted.

src/parse.js

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { decode64 } from './base64.js';
1+
import { decode85 } from './z85.js';
22
import {
33
HOLE,
44
NAN,
@@ -102,31 +102,26 @@ export function unflatten(parsed, revivers) {
102102
}
103103
break;
104104

105-
case "Int8Array":
106-
case "Uint8Array":
107-
case "Uint8ClampedArray":
108-
case "Int16Array":
109-
case "Uint16Array":
110-
case "Int32Array":
111-
case "Uint32Array":
112-
case "Float32Array":
113-
case "Float64Array":
114-
case "BigInt64Array":
115-
case "BigUint64Array": {
116-
const TypedArrayConstructor = globalThis[type];
117-
const base64 = value[1];
118-
const arraybuffer = decode64(base64);
119-
const typedArray = new TypedArrayConstructor(arraybuffer);
120-
hydrated[index] = typedArray;
121-
break;
122-
}
123-
124-
case "ArrayBuffer": {
125-
const base64 = value[1];
126-
const arraybuffer = decode64(base64);
127-
hydrated[index] = arraybuffer;
128-
break;
129-
}
105+
case "Int8Array":
106+
case "Uint8Array":
107+
case "Uint8ClampedArray":
108+
case "Int16Array":
109+
case "Uint16Array":
110+
case "Int32Array":
111+
case "Uint32Array":
112+
case "Float32Array":
113+
case "Float64Array":
114+
case "BigInt64Array":
115+
case "BigUint64Array": {
116+
const TypedArrayConstructor = globalThis[type];
117+
hydrated[index] = new TypedArrayConstructor(decode85(value[1]));
118+
break;
119+
}
120+
121+
case "ArrayBuffer": {
122+
hydrated[index] = decode85(value[1]);
123+
break;
124+
}
130125

131126
default:
132127
throw new Error(`Unknown type ${type}`);

src/stringify.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
POSITIVE_INFINITY,
1616
UNDEFINED
1717
} from './constants.js';
18-
import { encode64 } from './base64.js';
18+
import { encode85 } from './z85.js';
1919

2020
/**
2121
* Turn a value into a JSON string that can be parsed with `devalue.parse`
@@ -153,20 +153,18 @@ export function stringify(value, reducers) {
153153
case "BigUint64Array": {
154154
/** @type {import("./types.js").TypedArray} */
155155
const typedArray = thing;
156-
const base64 = encode64(typedArray.buffer);
157-
str = '["' + type + '","' + base64 + '"]';
156+
str = '["' + type + '","' + encode85(typedArray.buffer) + '"]';
158157
break;
159158
}
160-
159+
161160
case "ArrayBuffer": {
162161
/** @type {ArrayBuffer} */
163162
const arraybuffer = thing;
164-
const base64 = encode64(arraybuffer);
165-
166-
str = `["ArrayBuffer","${base64}"]`;
163+
164+
str = `["ArrayBuffer","${encode85(arraybuffer)}"]`;
167165
break;
168166
}
169-
167+
170168
default:
171169
if (!is_plain_object(thing)) {
172170
throw new DevalueError(

src/z85.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const CHARSET = new Uint8Array(`0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#`.split('').map(c => c.charCodeAt(0)));
2+
const POW_85 = [1, 85, 7225, 614125, 52200625];
3+
/** @type {Uint8Array} */
4+
let REVERSE_MAP;
5+
6+
/**
7+
* Encodes binary data into a Z85 string.
8+
* @param {ArrayBuffer} data - The binary data to encode
9+
* @returns {string} The Z85 encoded string
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);
17+
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;
54+
}
55+
}
56+
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+
}
73+
74+
/**
75+
* Decodes a Z85 string into binary data.
76+
* @param {string} string - The Z85 encoded string
77+
* @returns {ArrayBuffer} The decoded binary data
78+
*/
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+
}
120+
}
121+
122+
return result.buffer;
123+
}

test/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,13 @@ const fixtures = {
164164
name: 'Uint8Array',
165165
value: new Uint8Array([1, 2, 3]),
166166
js: 'new Uint8Array([1,2,3])',
167-
json: '[["Uint8Array","AQID"]]'
167+
json: '[["Uint8Array","0rJu"]]'
168168
},
169169
{
170170
name: 'ArrayBuffer',
171171
value: new Uint8Array([1, 2, 3]).buffer,
172172
js: 'new Uint8Array([1,2,3]).buffer',
173-
json: '[["ArrayBuffer","AQID"]]'
173+
json: '[["ArrayBuffer","0rJu"]]'
174174
}
175175
],
176176

0 commit comments

Comments
 (0)