Skip to content

Commit fed6331

Browse files
chore: refactor to consolidate base64 encoding functions (alternative) (#14160)
* chore: refactor redundant base64 encoding/decoding * changeset * mark slow test * add test, add buffer fallback * fix test * check Buffer on each run * probably check this every run too * singleton textencoder / textdecoder * simplify base64_encode and base64_decode * tweak * gotta go fast --------- Co-authored-by: Ottomated <[email protected]>
1 parent 9493537 commit fed6331

File tree

16 files changed

+126
-134
lines changed

16 files changed

+126
-134
lines changed

.changeset/petite-doors-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
chore: refactor redundant base64 encoding/decoding functions

packages/kit/src/exports/index.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
strip_data_suffix,
99
strip_resolution_suffix
1010
} from '../runtime/pathname.js';
11+
import { text_encoder } from '../runtime/utils.js';
1112

1213
export { VERSION } from '../version.js';
1314

@@ -142,7 +143,7 @@ export function json(data, init) {
142143
// means less duplicated work
143144
const headers = new Headers(init?.headers);
144145
if (!headers.has('content-length')) {
145-
headers.set('content-length', encoder.encode(body).byteLength.toString());
146+
headers.set('content-length', text_encoder.encode(body).byteLength.toString());
146147
}
147148

148149
if (!headers.has('content-type')) {
@@ -155,8 +156,6 @@ export function json(data, init) {
155156
});
156157
}
157158

158-
const encoder = new TextEncoder();
159-
160159
/**
161160
* Create a `Response` object from the supplied body.
162161
* @param {string} body The value that will be used as-is.
@@ -165,7 +164,7 @@ const encoder = new TextEncoder();
165164
export function text(body, init) {
166165
const headers = new Headers(init?.headers);
167166
if (!headers.has('content-length')) {
168-
const encoded = encoder.encode(body);
167+
const encoded = text_encoder.encode(body);
169168
headers.set('content-length', encoded.byteLength.toString());
170169
return new Response(encoded, {
171170
...init,

packages/kit/src/runtime/app/server/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { read_implementation, manifest } from '__sveltekit/server';
22
import { base } from '__sveltekit/paths';
33
import { DEV } from 'esm-env';
4-
import { b64_decode } from '../../utils.js';
4+
import { base64_decode } from '../../utils.js';
55

66
/**
77
* Read the contents of an imported asset from the filesystem
@@ -33,8 +33,9 @@ export function read(asset) {
3333
const data = asset.slice(match[0].length);
3434

3535
if (match[2] !== undefined) {
36-
const decoded = b64_decode(data);
36+
const decoded = base64_decode(data);
3737

38+
// @ts-ignore passing a Uint8Array to `new Response(...)` is fine
3839
return new Response(decoded, {
3940
headers: {
4041
'Content-Length': decoded.byteLength.toString(),

packages/kit/src/runtime/client/client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { get_message, get_status } from '../../utils/error.js';
4444
import { writable } from 'svelte/store';
4545
import { page, update, navigating } from './state.svelte.js';
4646
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
47+
import { text_decoder } from '../utils.js';
4748

4849
export { load_css };
4950
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
@@ -2781,7 +2782,6 @@ async function load_data(url, invalid) {
27812782
*/
27822783
const deferreds = new Map();
27832784
const reader = /** @type {ReadableStream<Uint8Array>} */ (res.body).getReader();
2784-
const decoder = new TextDecoder();
27852785

27862786
/**
27872787
* @param {any} data
@@ -2804,7 +2804,7 @@ async function load_data(url, invalid) {
28042804
const { done, value } = await reader.read();
28052805
if (done && !text) break;
28062806

2807-
text += !value && text ? '\n' : decoder.decode(value, { stream: true }); // no value -> final chunk -> add a new line to trigger the last parse
2807+
text += !value && text ? '\n' : text_decoder.decode(value, { stream: true }); // no value -> final chunk -> add a new line to trigger the last parse
28082808

28092809
while (true) {
28102810
const split = text.indexOf('\n');

packages/kit/src/runtime/client/fetcher.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BROWSER, DEV } from 'esm-env';
22
import { hash } from '../../utils/hash.js';
3-
import { b64_decode } from '../utils.js';
3+
import { base64_decode } from '../utils.js';
44

55
let loading = 0;
66

@@ -98,7 +98,7 @@ export function initial_fetch(resource, opts) {
9898
if (b64 !== null) {
9999
// Can't use native_fetch('data:...;base64,${body}')
100100
// csp can block the request
101-
body = b64_decode(body);
101+
body = base64_decode(body);
102102
}
103103

104104
return Promise.resolve(new Response(body, init));

packages/kit/src/runtime/server/cookie.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { parse, serialize } from 'cookie';
22
import { normalize_path, resolve } from '../../utils/url.js';
33
import { add_data_suffix } from '../pathname.js';
4+
import { text_encoder } from '../utils.js';
45

56
// eslint-disable-next-line no-control-regex -- control characters are invalid in cookie names
67
const INVALID_COOKIE_CHARACTER_REGEX = /[\x00-\x1F\x7F()<>@,;:"/[\]?={} \t]/;
@@ -217,7 +218,7 @@ export function get_cookies(request, url) {
217218

218219
if (__SVELTEKIT_DEV__) {
219220
const serialized = serialize(name, value, new_cookies[name].options);
220-
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) {
221+
if (text_encoder.encode(serialized).byteLength > MAX_COOKIE_SIZE) {
221222
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
222223
}
223224

packages/kit/src/runtime/server/data/index.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from
77
import { normalize_path } from '../../../utils/url.js';
88
import * as devalue from 'devalue';
99
import { create_async_iterator } from '../../../utils/streaming.js';
10-
11-
const encoder = new TextEncoder();
10+
import { text_encoder } from '../../utils.js';
1211

1312
/**
1413
* @param {import('@sveltejs/kit').RequestEvent} event
@@ -129,9 +128,9 @@ export async function render_data(
129128
return new Response(
130129
new ReadableStream({
131130
async start(controller) {
132-
controller.enqueue(encoder.encode(data));
131+
controller.enqueue(text_encoder.encode(data));
133132
for await (const chunk of chunks) {
134-
controller.enqueue(encoder.encode(chunk));
133+
controller.enqueue(text_encoder.encode(chunk));
135134
}
136135
controller.close();
137136
},

packages/kit/src/runtime/server/page/crypto.js

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const encoder = new TextEncoder();
1+
import { text_encoder } from '../../utils.js';
22

33
/**
44
* SHA-256 hashing function adapted from https://bitwiseshiftleft.github.io/sjcl
@@ -102,7 +102,7 @@ export function sha256(data) {
102102
const bytes = new Uint8Array(out.buffer);
103103
reverse_endianness(bytes);
104104

105-
return base64(bytes);
105+
return btoa(String.fromCharCode(...bytes));
106106
}
107107

108108
/** The SHA-256 initialization vector */
@@ -160,7 +160,7 @@ function reverse_endianness(bytes) {
160160

161161
/** @param {string} str */
162162
function encode(str) {
163-
const encoded = encoder.encode(str);
163+
const encoded = text_encoder.encode(str);
164164
const length = encoded.length * 8;
165165

166166
// result should be a multiple of 512 bits in length,
@@ -182,58 +182,3 @@ function encode(str) {
182182

183183
return words;
184184
}
185-
186-
/*
187-
Based on https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
188-
189-
MIT License
190-
Copyright (c) 2020 Egor Nepomnyaschih
191-
Permission is hereby granted, free of charge, to any person obtaining a copy
192-
of this software and associated documentation files (the "Software"), to deal
193-
in the Software without restriction, including without limitation the rights
194-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
195-
copies of the Software, and to permit persons to whom the Software is
196-
furnished to do so, subject to the following conditions:
197-
The above copyright notice and this permission notice shall be included in all
198-
copies or substantial portions of the Software.
199-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
200-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
201-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
202-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
203-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
204-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
205-
SOFTWARE.
206-
*/
207-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
208-
209-
/** @param {Uint8Array} bytes */
210-
export function base64(bytes) {
211-
const l = bytes.length;
212-
213-
let result = '';
214-
let i;
215-
216-
for (i = 2; i < l; i += 3) {
217-
result += chars[bytes[i - 2] >> 2];
218-
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
219-
result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
220-
result += chars[bytes[i] & 0x3f];
221-
}
222-
223-
if (i === l + 1) {
224-
// 1 octet yet to write
225-
result += chars[bytes[i - 2] >> 2];
226-
result += chars[(bytes[i - 2] & 0x03) << 4];
227-
result += '==';
228-
}
229-
230-
if (i === l) {
231-
// 2 octets yet to write
232-
result += chars[bytes[i - 2] >> 2];
233-
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
234-
result += chars[(bytes[i - 1] & 0x0f) << 2];
235-
result += '=';
236-
}
237-
238-
return result;
239-
}

packages/kit/src/runtime/server/page/crypto.spec.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { webcrypto } from 'node:crypto';
22
import { assert, test } from 'vitest';
33
import { sha256 } from './crypto.js';
4+
import { text_encoder } from '../../utils.js';
45

56
const inputs = [
67
'hello world',
78
'',
89
'abcd',
910
'the quick brown fox jumps over the lazy dog',
1011
'工欲善其事,必先利其器'
11-
].slice(0);
12+
];
1213

1314
inputs.forEach((input) => {
1415
test(input, async () => {
15-
const expected_bytes = await webcrypto.subtle.digest(
16-
'SHA-256',
17-
new TextEncoder().encode(input)
18-
);
16+
const expected_bytes = await webcrypto.subtle.digest('SHA-256', text_encoder.encode(input));
1917
const expected = Buffer.from(expected_bytes).toString('base64');
2018

2119
const actual = sha256(input);

packages/kit/src/runtime/server/page/csp.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { escape_html } from '../../../utils/escape.js';
2-
import { base64, sha256 } from './crypto.js';
2+
import { sha256 } from './crypto.js';
33

44
const array = new Uint8Array(16);
55

66
function generate_nonce() {
77
crypto.getRandomValues(array);
8-
return base64(array);
8+
return btoa(String.fromCharCode(...array));
99
}
1010

1111
const quoted = new Set([

0 commit comments

Comments
 (0)