|
2 | 2 | // This module is browser compatible.
|
3 | 3 |
|
4 | 4 | /**
|
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. |
6 | 6 | *
|
7 | 7 | * ```ts
|
8 | 8 | * 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"; |
11 | 12 | *
|
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" })); |
15 | 16 | *
|
16 |
| - * assertEquals(await toText(stream), "Hello World!"); |
| 17 | + * assertEquals( |
| 18 | + * await toText(readable), |
| 19 | + * encodeBase32(await Deno.readFile("./deno.lock"), "Base32"), |
| 20 | + * ); |
17 | 21 | * ```
|
18 | 22 | *
|
19 | 23 | * @experimental **UNSTABLE**: New API, yet to be vetted.
|
20 | 24 | *
|
21 | 25 | * @module
|
22 | 26 | */
|
23 | 27 |
|
24 |
| -import { decodeBase32, encodeBase32 } from "./base32.ts"; |
25 | 28 | import type { Uint8Array_ } from "./_types.ts";
|
26 | 29 | 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; |
27 | 39 |
|
28 | 40 | /**
|
29 |
| - * Converts a Uint8Array stream into a base32-encoded stream. |
| 41 | + * Transforms a {@linkcode Uint8Array<ArrayBuffer>} stream into a base32 stream. |
30 | 42 | *
|
31 | 43 | * @experimental **UNSTABLE**: New API, yet to be vetted.
|
32 | 44 | *
|
33 |
| - * @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-6} |
| 45 | + * @typeParam T The type of the base32 stream. |
34 | 46 | *
|
35 |
| - * @example Usage |
| 47 | + * @example Basic Usage |
36 | 48 | * ```ts
|
37 | 49 | * import { assertEquals } from "@std/assert";
|
38 |
| - * import { encodeBase32 } from "@std/encoding/base32"; |
| 50 | + * import { encodeBase32 } from "@std/encoding/unstable-base32"; |
39 | 51 | * import { Base32EncoderStream } from "@std/encoding/unstable-base32-stream";
|
40 |
| - * import { toText } from "@std/streams/to-text"; |
| 52 | + * import { toText } from "@std/streams"; |
41 | 53 | *
|
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" })); |
45 | 57 | *
|
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 | + * ); |
47 | 62 | * ```
|
48 | 63 | */
|
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; |
52 | 82 | super({
|
53 | 83 | 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, |
61 | 96 | );
|
62 |
| - push = remainder ? concat.slice(remainder) : new Uint8Array(0); |
| 97 | + controller.enqueue(decode(output.subarray(0, o))); |
63 | 98 | },
|
64 | 99 | 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)); |
67 | 107 | }
|
68 | 108 | },
|
69 | 109 | });
|
70 | 110 | }
|
71 | 111 | }
|
72 | 112 |
|
73 | 113 | /**
|
74 |
| - * Decodes a base32-encoded stream into a Uint8Array stream. |
| 114 | + * Transforms a base32 stream into a {@link Uint8Array<ArrayBuffer>} stream. |
75 | 115 | *
|
76 | 116 | * @experimental **UNSTABLE**: New API, yet to be vetted.
|
77 | 117 | *
|
78 |
| - * @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-6} |
| 118 | + * @typeParam T The type of the base32 stream. |
79 | 119 | *
|
80 |
| - * @example Usage |
| 120 | + * @example Basic Usage |
81 | 121 | * ```ts
|
82 | 122 | * 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"; |
85 | 128 | *
|
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" })); |
89 | 133 | *
|
90 |
| - * assertEquals(await toText(stream), "Hello World!"); |
| 134 | + * assertEquals( |
| 135 | + * await toBytes(readable), |
| 136 | + * await Deno.readFile("./deno.lock"), |
| 137 | + * ); |
91 | 138 | * ```
|
| 139 | + * |
| 140 | + * @module |
92 | 141 | */
|
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; |
96 | 160 | super({
|
97 | 161 | 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)); |
101 | 166 | }
|
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)); |
105 | 176 | },
|
106 | 177 | 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)); |
109 | 186 | }
|
110 | 187 | },
|
111 | 188 | });
|
|
0 commit comments