Skip to content

Commit bcb30bb

Browse files
authored
BREAKING(encoding/unstable): merge Base32Hex(Encoder|Decoder)Stream to Base32(Encoder|Decoder)Stream (#6452)
1 parent da31f7a commit bcb30bb

6 files changed

+207
-232
lines changed

_tools/check_docs.ts

-2
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ const ENTRY_POINTS = [
5151
"../encoding/unstable_base32.ts",
5252
"../encoding/unstable_base64.ts",
5353
"../encoding/unstable_base64_stream.ts",
54-
"../encoding/unstable_base32hex_stream.ts",
5554
"../encoding/unstable_base32_stream.ts",
56-
"../encoding/unstable_base32hex_stream.ts",
5755
"../encoding/unstable_hex_stream.ts",
5856
"../expect/mod.ts",
5957
"../fmt/bytes.ts",

encoding/deno.json

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"./base32": "./base32.ts",
88
"./unstable-base32": "./unstable_base32.ts",
99
"./unstable-base32-stream": "./unstable_base32_stream.ts",
10-
"./unstable-base32hex-stream": "./unstable_base32hex_stream.ts",
1110
"./base58": "./base58.ts",
1211
"./base64": "./base64.ts",
1312
"./unstable-base64": "./unstable_base64.ts",

encoding/unstable_base32_stream.ts

+127-50
Original file line numberDiff line numberDiff line change
@@ -2,110 +2,187 @@
22
// This module is browser compatible.
33

44
/**
5-
* Utilities for encoding and decoding to and from base32 in a streaming manner.
5+
* TransformStream classes to encode and decode to and from base32 data in a streaming manner.
66
*
77
* ```ts
88
* import { assertEquals } from "@std/assert";
9-
* import { Base32DecoderStream } from "@std/encoding/unstable-base32-stream";
10-
* import { toText } from "@std/streams/to-text";
9+
* import { encodeBase32 } from "@std/encoding/unstable-base32";
10+
* import { Base32EncoderStream } from "@std/encoding/unstable-base32-stream";
11+
* import { toText } from "@std/streams";
1112
*
12-
* const stream = ReadableStream.from(["JBSWY3DPEBLW64TMMQQQ===="])
13-
* .pipeThrough(new Base32DecoderStream())
14-
* .pipeThrough(new TextDecoderStream());
13+
* const readable = (await Deno.open("./deno.lock"))
14+
* .readable
15+
* .pipeThrough(new Base32EncoderStream({ output: "string" }));
1516
*
16-
* assertEquals(await toText(stream), "Hello World!");
17+
* assertEquals(
18+
* await toText(readable),
19+
* encodeBase32(await Deno.readFile("./deno.lock"), "Base32"),
20+
* );
1721
* ```
1822
*
1923
* @experimental **UNSTABLE**: New API, yet to be vetted.
2024
*
2125
* @module
2226
*/
2327

24-
import { decodeBase32, encodeBase32 } from "./base32.ts";
2528
import type { Uint8Array_ } from "./_types.ts";
2629
export type { Uint8Array_ };
30+
import {
31+
type Base32Format,
32+
calcMax,
33+
decodeRawBase32 as decode,
34+
encodeRawBase32 as encode,
35+
} from "./unstable_base32.ts";
36+
import { detach } from "./_common_detach.ts";
37+
38+
type Expect<T> = T extends "bytes" ? Uint8Array_ : string;
2739

2840
/**
29-
* Converts a Uint8Array stream into a base32-encoded stream.
41+
* Transforms a {@linkcode Uint8Array<ArrayBuffer>} stream into a base32 stream.
3042
*
3143
* @experimental **UNSTABLE**: New API, yet to be vetted.
3244
*
33-
* @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-6}
45+
* @typeParam T The type of the base32 stream.
3446
*
35-
* @example Usage
47+
* @example Basic Usage
3648
* ```ts
3749
* import { assertEquals } from "@std/assert";
38-
* import { encodeBase32 } from "@std/encoding/base32";
50+
* import { encodeBase32 } from "@std/encoding/unstable-base32";
3951
* import { Base32EncoderStream } from "@std/encoding/unstable-base32-stream";
40-
* import { toText } from "@std/streams/to-text";
52+
* import { toText } from "@std/streams";
4153
*
42-
* const stream = ReadableStream.from(["Hello,", " world!"])
43-
* .pipeThrough(new TextEncoderStream())
44-
* .pipeThrough(new Base32EncoderStream());
54+
* const readable = (await Deno.open("./deno.lock"))
55+
* .readable
56+
* .pipeThrough(new Base32EncoderStream({ output: "string" }));
4557
*
46-
* assertEquals(await toText(stream), encodeBase32(new TextEncoder().encode("Hello, world!")));
58+
* assertEquals(
59+
* await toText(readable),
60+
* encodeBase32(await Deno.readFile("./deno.lock"), "Base32"),
61+
* );
4762
* ```
4863
*/
49-
export class Base32EncoderStream extends TransformStream<Uint8Array, string> {
50-
constructor() {
51-
let push = new Uint8Array(0);
64+
export class Base32EncoderStream<T extends "string" | "bytes">
65+
extends TransformStream<
66+
Uint8Array_,
67+
T extends "bytes" ? Uint8Array_ : string
68+
> {
69+
/**
70+
* Constructs a new instance.
71+
*
72+
* @param options The options of the base32 stream.
73+
*/
74+
constructor(options: { format?: Base32Format; output?: T } = {}) {
75+
const decode = function (): (input: Uint8Array_) => Expect<T> {
76+
if (options.output === "bytes") return (x) => x as Expect<T>;
77+
const decoder = new TextDecoder();
78+
return (x) => decoder.decode(x) as Expect<T>;
79+
}();
80+
const push = new Uint8Array(4);
81+
let remainder = 0;
5282
super({
5383
transform(chunk, controller) {
54-
const concat = new Uint8Array(push.length + chunk.length);
55-
concat.set(push);
56-
concat.set(chunk, push.length);
57-
58-
const remainder = -concat.length % 5;
59-
controller.enqueue(
60-
encodeBase32(concat.slice(0, remainder || undefined)),
84+
let [output, i] = detach(chunk, calcMax(remainder + chunk.length));
85+
if (remainder) {
86+
i -= remainder;
87+
output.set(push.subarray(0, remainder), i);
88+
}
89+
remainder = (output.length - i) % 5;
90+
if (remainder) push.set(output.subarray(-remainder));
91+
const o = encode(
92+
output.subarray(0, -remainder || undefined),
93+
i,
94+
0,
95+
options.format,
6196
);
62-
push = remainder ? concat.slice(remainder) : new Uint8Array(0);
97+
controller.enqueue(decode(output.subarray(0, o)));
6398
},
6499
flush(controller) {
65-
if (push.length) {
66-
controller.enqueue(encodeBase32(push));
100+
if (remainder) {
101+
const [output, i] = detach(
102+
push.subarray(0, remainder),
103+
calcMax(remainder),
104+
);
105+
encode(output, i, 0, options.format);
106+
controller.enqueue(decode(output));
67107
}
68108
},
69109
});
70110
}
71111
}
72112

73113
/**
74-
* Decodes a base32-encoded stream into a Uint8Array stream.
114+
* Transforms a base32 stream into a {@link Uint8Array<ArrayBuffer>} stream.
75115
*
76116
* @experimental **UNSTABLE**: New API, yet to be vetted.
77117
*
78-
* @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-6}
118+
* @typeParam T The type of the base32 stream.
79119
*
80-
* @example Usage
120+
* @example Basic Usage
81121
* ```ts
82122
* import { assertEquals } from "@std/assert";
83-
* import { Base32DecoderStream } from "@std/encoding/unstable-base32-stream";
84-
* import { toText } from "@std/streams/to-text";
123+
* import {
124+
* Base32DecoderStream,
125+
* Base32EncoderStream,
126+
* } from "@std/encoding/unstable-base32-stream";
127+
* import { toBytes } from "@std/streams/unstable-to-bytes";
85128
*
86-
* const stream = ReadableStream.from(["JBSWY3DPEBLW64TMMQQQ===="])
87-
* .pipeThrough(new Base32DecoderStream())
88-
* .pipeThrough(new TextDecoderStream());
129+
* const readable = (await Deno.open("./deno.lock"))
130+
* .readable
131+
* .pipeThrough(new Base32EncoderStream({ output: "string" }))
132+
* .pipeThrough(new Base32DecoderStream({ input: "string" }));
89133
*
90-
* assertEquals(await toText(stream), "Hello World!");
134+
* assertEquals(
135+
* await toBytes(readable),
136+
* await Deno.readFile("./deno.lock"),
137+
* );
91138
* ```
139+
*
140+
* @module
92141
*/
93-
export class Base32DecoderStream extends TransformStream<string, Uint8Array_> {
94-
constructor() {
95-
let push = "";
142+
export class Base32DecoderStream<T extends "string" | "bytes">
143+
extends TransformStream<
144+
T extends "bytes" ? Uint8Array_ : string,
145+
Uint8Array_
146+
> {
147+
/**
148+
* Constructs a new instance.
149+
*
150+
* @param options The options of the base32 stream.
151+
*/
152+
constructor(options: { format?: Base32Format; input?: T } = {}) {
153+
const encode = function (): (input: Expect<T>) => Uint8Array_ {
154+
if (options.input === "bytes") return (x) => x as Uint8Array_;
155+
const encoder = new TextEncoder();
156+
return (x) => encoder.encode(x as string) as Uint8Array_;
157+
}();
158+
const push = new Uint8Array(7);
159+
let remainder = 0;
96160
super({
97161
transform(chunk, controller) {
98-
push += chunk;
99-
if (push.length < 8) {
100-
return;
162+
let output = encode(chunk);
163+
if (remainder) {
164+
output = detach(output, remainder + output.length)[0];
165+
output.set(push.subarray(0, remainder));
101166
}
102-
const remainder = -push.length % 8;
103-
controller.enqueue(decodeBase32(push.slice(0, remainder || undefined)));
104-
push = remainder ? chunk.slice(remainder) : "";
167+
remainder = output.length % 8;
168+
if (remainder) push.set(output.subarray(-remainder));
169+
const o = decode(
170+
output.subarray(0, -remainder || undefined),
171+
0,
172+
0,
173+
options.format,
174+
);
175+
controller.enqueue(output.subarray(0, o));
105176
},
106177
flush(controller) {
107-
if (push.length) {
108-
controller.enqueue(decodeBase32(push));
178+
if (remainder) {
179+
const o = decode(
180+
push.subarray(0, remainder),
181+
0,
182+
0,
183+
options.format,
184+
);
185+
controller.enqueue(push.subarray(0, o));
109186
}
110187
},
111188
});
+80-26
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,90 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

33
import { assertEquals } from "@std/assert";
4-
import { encodeBase32 } from "./base32.ts";
4+
import { toText } from "@std/streams";
5+
import { toBytes } from "@std/streams/unstable-to-bytes";
6+
import { FixedChunkStream } from "@std/streams/unstable-fixed-chunk-stream";
7+
import { encodeBase32 } from "./unstable_base32.ts";
58
import {
69
Base32DecoderStream,
710
Base32EncoderStream,
811
} from "./unstable_base32_stream.ts";
9-
import { RandomSliceStream } from "./_random_slice_stream.ts";
10-
import { toText } from "../streams/to_text.ts";
11-
import { concat } from "@std/bytes/concat";
12-
13-
Deno.test("Base32EncoderStream() encodes stream", async () => {
14-
const readable = (await Deno.open("./deno.lock"))
15-
.readable
16-
.pipeThrough(new RandomSliceStream())
17-
.pipeThrough(new Base32EncoderStream());
18-
19-
assertEquals(
20-
await toText(readable),
21-
encodeBase32(await Deno.readFile("./deno.lock")),
22-
);
12+
13+
Deno.test("Base32EncoderStream() with normal format", async () => {
14+
for (const format of ["Base32", "Base32Hex", "Base32Crockford"] as const) {
15+
const readable = (await Deno.open("./deno.lock"))
16+
.readable
17+
.pipeThrough(new FixedChunkStream(1021))
18+
.pipeThrough(new Base32EncoderStream({ format, output: "string" }));
19+
20+
assertEquals(
21+
await toText(readable),
22+
encodeBase32(await Deno.readFile("./deno.lock"), format),
23+
format,
24+
);
25+
}
26+
});
27+
28+
Deno.test("Base32EncoderStream() with raw format", async () => {
29+
for (
30+
const format of [
31+
"Base32",
32+
"Base32Hex",
33+
"Base32Crockford",
34+
] as const
35+
) {
36+
const readable = (await Deno.open("./deno.lock"))
37+
.readable
38+
.pipeThrough(new FixedChunkStream(1021))
39+
.pipeThrough(new Base32EncoderStream({ format, output: "bytes" }));
40+
41+
assertEquals(
42+
await toBytes(readable),
43+
new TextEncoder().encode(
44+
encodeBase32(
45+
await Deno.readFile("./deno.lock"),
46+
format,
47+
),
48+
),
49+
format,
50+
);
51+
}
2352
});
2453

25-
Deno.test("Base32DecoderStream() decodes stream", async () => {
26-
const readable = (await Deno.open("./deno.lock"))
27-
.readable
28-
.pipeThrough(new Base32EncoderStream())
29-
.pipeThrough(new RandomSliceStream())
30-
.pipeThrough(new Base32DecoderStream());
31-
32-
assertEquals(
33-
concat(await Array.fromAsync(readable)),
34-
await Deno.readFile("./deno.lock"),
35-
);
54+
Deno.test("Base32DecoderStream() with normal format", async () => {
55+
for (const format of ["Base32", "Base32Hex", "Base32Crockford"] as const) {
56+
const readable = (await Deno.open("./deno.lock"))
57+
.readable
58+
.pipeThrough(new Base32EncoderStream({ format, output: "string" }))
59+
.pipeThrough(new TextEncoderStream())
60+
.pipeThrough(new FixedChunkStream(1021))
61+
.pipeThrough(new TextDecoderStream())
62+
.pipeThrough(new Base32DecoderStream({ format, input: "string" }));
63+
64+
assertEquals(
65+
await toBytes(readable),
66+
await Deno.readFile("./deno.lock"),
67+
);
68+
}
69+
});
70+
71+
Deno.test("Base32DecoderStream() with raw format", async () => {
72+
for (
73+
const format of [
74+
"Base32",
75+
"Base32Hex",
76+
"Base32Crockford",
77+
] as const
78+
) {
79+
const readable = (await Deno.open("./deno.lock"))
80+
.readable
81+
.pipeThrough(new Base32EncoderStream({ format, output: "bytes" }))
82+
.pipeThrough(new FixedChunkStream(1021))
83+
.pipeThrough(new Base32DecoderStream({ format, input: "bytes" }));
84+
85+
assertEquals(
86+
await toBytes(readable),
87+
await Deno.readFile("./deno.lock"),
88+
);
89+
}
3690
});

0 commit comments

Comments
 (0)