Skip to content

chore: refactor to consolidate base64 encoding functions (alternative) #14160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/petite-doors-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

chore: refactor redundant base64 encoding/decoding functions
7 changes: 3 additions & 4 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
strip_data_suffix,
strip_resolution_suffix
} from '../runtime/pathname.js';
import { text_encoder } from '../runtime/utils.js';

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

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

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

const encoder = new TextEncoder();

/**
* Create a `Response` object from the supplied body.
* @param {string} body The value that will be used as-is.
Expand All @@ -165,7 +164,7 @@ const encoder = new TextEncoder();
export function text(body, init) {
const headers = new Headers(init?.headers);
if (!headers.has('content-length')) {
const encoded = encoder.encode(body);
const encoded = text_encoder.encode(body);
headers.set('content-length', encoded.byteLength.toString());
return new Response(encoded, {
...init,
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/app/server/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { read_implementation, manifest } from '__sveltekit/server';
import { base } from '__sveltekit/paths';
import { DEV } from 'esm-env';
import { b64_decode } from '../../utils.js';
import { base64_decode } from '../../utils.js';

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

if (match[2] !== undefined) {
const decoded = b64_decode(data);
const decoded = base64_decode(data);

// @ts-ignore passing a Uint8Array to `new Response(...)` is fine
return new Response(decoded, {
headers: {
'Content-Length': decoded.byteLength.toString(),
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { get_message, get_status } from '../../utils/error.js';
import { writable } from 'svelte/store';
import { page, update, navigating } from './state.svelte.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
import { text_decoder } from '../utils.js';

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

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

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

while (true) {
const split = text.indexOf('\n');
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/fetcher.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BROWSER, DEV } from 'esm-env';
import { hash } from '../../utils/hash.js';
import { b64_decode } from '../utils.js';
import { base64_decode } from '../utils.js';

let loading = 0;

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

return Promise.resolve(new Response(body, init));
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/cookie.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parse, serialize } from 'cookie';
import { normalize_path, resolve } from '../../utils/url.js';
import { add_data_suffix } from '../pathname.js';
import { text_encoder } from '../utils.js';

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

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

Expand Down
7 changes: 3 additions & 4 deletions packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from
import { normalize_path } from '../../../utils/url.js';
import * as devalue from 'devalue';
import { create_async_iterator } from '../../../utils/streaming.js';

const encoder = new TextEncoder();
import { text_encoder } from '../../utils.js';

/**
* @param {import('@sveltejs/kit').RequestEvent} event
Expand Down Expand Up @@ -129,9 +128,9 @@ export async function render_data(
return new Response(
new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode(data));
controller.enqueue(text_encoder.encode(data));
for await (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk));
controller.enqueue(text_encoder.encode(chunk));
}
controller.close();
},
Expand Down
61 changes: 3 additions & 58 deletions packages/kit/src/runtime/server/page/crypto.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const encoder = new TextEncoder();
import { text_encoder } from '../../utils.js';

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

return base64(bytes);
return btoa(String.fromCharCode(...bytes));
}

/** The SHA-256 initialization vector */
Expand Down Expand Up @@ -160,7 +160,7 @@ function reverse_endianness(bytes) {

/** @param {string} str */
function encode(str) {
const encoded = encoder.encode(str);
const encoded = text_encoder.encode(str);
const length = encoded.length * 8;

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

return words;
}

/*
Based on https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727

MIT License
Copyright (c) 2020 Egor Nepomnyaschih
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');

/** @param {Uint8Array} bytes */
export function base64(bytes) {
const l = bytes.length;

let result = '';
let i;

for (i = 2; i < l; i += 3) {
result += chars[bytes[i - 2] >> 2];
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
result += chars[bytes[i] & 0x3f];
}

if (i === l + 1) {
// 1 octet yet to write
result += chars[bytes[i - 2] >> 2];
result += chars[(bytes[i - 2] & 0x03) << 4];
result += '==';
}

if (i === l) {
// 2 octets yet to write
result += chars[bytes[i - 2] >> 2];
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += chars[(bytes[i - 1] & 0x0f) << 2];
result += '=';
}

return result;
}
8 changes: 3 additions & 5 deletions packages/kit/src/runtime/server/page/crypto.spec.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { webcrypto } from 'node:crypto';
import { assert, test } from 'vitest';
import { sha256 } from './crypto.js';
import { text_encoder } from '../../utils.js';

const inputs = [
'hello world',
'',
'abcd',
'the quick brown fox jumps over the lazy dog',
'工欲善其事,必先利其器'
].slice(0);
];

inputs.forEach((input) => {
test(input, async () => {
const expected_bytes = await webcrypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(input)
);
const expected_bytes = await webcrypto.subtle.digest('SHA-256', text_encoder.encode(input));
const expected = Buffer.from(expected_bytes).toString('base64');

const actual = sha256(input);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { escape_html } from '../../../utils/escape.js';
import { base64, sha256 } from './crypto.js';
import { sha256 } from './crypto.js';

const array = new Uint8Array(16);

function generate_nonce() {
crypto.getRandomValues(array);
return base64(array);
return btoa(String.fromCharCode(...array));
}

const quoted = new Set([
Expand Down
11 changes: 6 additions & 5 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DEV } from 'esm-env';
import { disable_search, make_trackable } from '../../../utils/url.js';
import { validate_depends } from '../../shared.js';
import { b64_encode } from '../../utils.js';
import { base64_encode, text_decoder } from '../../utils.js';
import { with_event } from '../../app/server/event.js';

/**
Expand Down Expand Up @@ -316,12 +316,14 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
return async () => {
const buffer = await response.arrayBuffer();

const bytes = new Uint8Array(buffer);

if (dependency) {
dependency.body = new Uint8Array(buffer);
dependency.body = bytes;
}

if (buffer instanceof ArrayBuffer) {
await push_fetched(b64_encode(buffer), true);
await push_fetched(base64_encode(bytes), true);
}

return buffer;
Expand Down Expand Up @@ -394,13 +396,12 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
async function stream_to_string(stream) {
let result = '';
const reader = stream.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
result += decoder.decode(value);
result += text_decoder.decode(value);
}
return result;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { create_server_routing_response, generate_route_object } from './server_
import { add_resolution_suffix } from '../../pathname.js';
import { with_event } from '../../app/server/event.js';
import { get_event_state } from '../event-state.js';
import { text_encoder } from '../../utils.js';

// TODO rename this function/module

Expand All @@ -25,8 +26,6 @@ const updated = {
check: () => false
};

const encoder = new TextEncoder();

/**
* Creates the HTML response.
* @param {{
Expand Down Expand Up @@ -586,9 +585,9 @@ export async function render_response({
: new Response(
new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode(transformed + '\n'));
controller.enqueue(text_encoder.encode(transformed + '\n'));
for await (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk));
controller.enqueue(text_encoder.encode(chunk));
}
controller.close();
},
Expand Down
20 changes: 8 additions & 12 deletions packages/kit/src/runtime/shared.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @import { Transport } from '@sveltejs/kit' */
import * as devalue from 'devalue';
import { base64_decode, base64_encode, text_decoder } from './utils.js';

/**
* @param {string} route_id
Expand Down Expand Up @@ -41,12 +42,8 @@ export function stringify_remote_arg(value, transport) {
// If people hit file/url size limits, we can look into using something like compress_and_encode_text from svelte.dev beyond a certain size
const json_string = stringify(value, transport);

// Convert to UTF-8 bytes, then base64 - handles all Unicode properly (btoa would fail on exotic characters)
const utf8_bytes = new TextEncoder().encode(json_string);
return btoa(String.fromCharCode(...utf8_bytes))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const bytes = new TextEncoder().encode(json_string);
return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
}

/**
Expand All @@ -57,13 +54,12 @@ export function stringify_remote_arg(value, transport) {
export function parse_remote_arg(string, transport) {
if (!string) return undefined;

const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
const json_string = text_decoder.decode(
// no need to add back `=` characters, atob can handle it
base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
);

// We don't need to add back the `=`-padding because atob can handle it
const base64_restored = string.replace(/-/g, '+').replace(/_/g, '/');
const binary_string = atob(base64_restored);
const utf8_bytes = new Uint8Array([...binary_string].map((char) => char.charCodeAt(0)));
const json_string = new TextDecoder().decode(utf8_bytes);
const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));

return devalue.parse(json_string, decoders);
}
Expand Down
Loading
Loading