Skip to content
Open
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/fresh-shoes-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

chore: reuse base64 and text decoding helpers
1 change: 1 addition & 0 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const files = fileURLToPath(new URL('./files', import.meta.url).href);

/** @param {string} str */
function escape_regex(str) {
// TODO replace with `RegExp.escape(str)` when we require Node >= 24
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export function get_link_info(a, base, uses_hash_router) {
/** @type {URL | undefined} */
let url;

// TODO replace the try/catch with `URL.parse` when browser support allows (Chrome 126, Firefox 126, Safari 18)
try {
url = new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);

Expand Down
10 changes: 4 additions & 6 deletions packages/kit/src/runtime/form-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@

import { DEV } from 'esm-env';
import * as devalue from 'devalue';
import { text_encoder } from './utils.js';
import { text_decoder, text_encoder } from './utils.js';
import { noop } from '../utils/functions.js';
import { SvelteKitError } from '@sveltejs/kit/internal';

const decoder = new TextDecoder();

/**
* Sets a parsed form field value in a nested object, mutating the original object.
* @param {Record<string, any>} object
Expand Down Expand Up @@ -283,7 +281,7 @@ export async function deserialize_binary_form(request, form_id) {
const file_offsets_buffer = await get_buffer(HEADER_BYTES + data_length, file_offsets_length);
if (!file_offsets_buffer) throw deserialize_error('file offset table too short');

const parsed_offsets = JSON.parse(decoder.decode(file_offsets_buffer));
const parsed_offsets = JSON.parse(text_decoder.decode(file_offsets_buffer));

if (
!Array.isArray(parsed_offsets) ||
Expand All @@ -298,7 +296,7 @@ export async function deserialize_binary_form(request, form_id) {

/** @type {Array<{ offset: number, size: number }>} */
const file_spans = [];
const [data, meta] = devalue.parse(decoder.decode(data_buffer), {
const [data, meta] = devalue.parse(text_decoder.decode(data_buffer), {
File: ([name, type, size, last_modified, index]) => {
if (
typeof name !== 'string' ||
Expand Down Expand Up @@ -488,7 +486,7 @@ class LazyFile {
});
}
async text() {
return decoder.decode(await this.arrayBuffer());
return text_decoder.decode(await this.arrayBuffer());
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/server/page/crypto.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { text_encoder } from '../../utils.js';
import { base64_encode, 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 btoa(String.fromCharCode(...bytes));
return base64_encode(bytes);
}

/** The SHA-256 initialization vector */
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { escape_html } from '../../../utils/escape.js';
import { base64_encode } from '../../utils.js';
import { sha256 } from './crypto.js';

const array = new Uint8Array(16);

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

const quoted = new Set([
Expand Down
31 changes: 2 additions & 29 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)

const request_body =
input instanceof Request && cloned_body
? await stream_to_string(cloned_body)
? await new Response(cloned_body).text()
: init?.body;

if (
Expand Down Expand Up @@ -371,16 +371,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
const [a, b] = response.body.tee();

void (async () => {
let result = new Uint8Array();

for await (const chunk of a) {
const combined = new Uint8Array(result.length + chunk.length);

combined.set(result, 0);
combined.set(chunk, result.length);

result = combined;
}
const result = new Uint8Array(await new Response(a).arrayBuffer());

if (dependency) {
dependency.body = new Uint8Array(result);
Expand Down Expand Up @@ -496,21 +487,3 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
return response;
};
}

/**
* @param {ReadableStream<Uint8Array>} stream
*/
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) {
result += decoder.decode();
break;
}
result += decoder.decode(value, { stream: true });
}
return result;
}
6 changes: 4 additions & 2 deletions packages/kit/src/runtime/shared.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @import { Transport } from '@sveltejs/kit' */
import * as devalue from 'devalue';
import { base64_decode, base64_encode, text_encoder } from './utils.js';
import { base64_decode, base64_encode, text_decoder, text_encoder } from './utils.js';

/**
* @param {string} route_id
Expand Down Expand Up @@ -331,6 +331,7 @@ export async function stringify_command_arg(value, transport) {
*/
function url_friendly_base64_encode(string) {
const bytes = text_encoder.encode(string);
// TODO replace with `bytes.toBase64({ alphabet: 'base64url', omitPadding: true })` when we require Node >= 25
return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
}

Expand All @@ -342,7 +343,8 @@ function url_friendly_base64_encode(string) {
export function parse_remote_arg(string, transport) {
if (!string) return undefined;

const json_string = new TextDecoder().decode(
const json_string = text_decoder.decode(
// TODO replace with `Uint8Array.fromBase64(string, { alphabet: 'base64url' })` when we require Node >= 25
// no need to add back `=` characters, atob can handle it
base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
);
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/runtime/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BROWSER } from 'esm-env';

export const text_encoder = new TextEncoder();
export const text_decoder = new TextDecoder();

/**
* Like node's path.relative, but without using node
Expand Down Expand Up @@ -28,6 +29,7 @@ export function get_relative_path(from, to) {
* @returns {string}
*/
export function base64_encode(bytes) {
// TODO replace with `bytes.toBase64()` when we require Node >= 25
// Using `Buffer` is faster than iterating
if (!BROWSER && globalThis.Buffer) {
return globalThis.Buffer.from(bytes).toString('base64');
Expand All @@ -47,6 +49,7 @@ export function base64_encode(bytes) {
* @returns {Uint8Array}
*/
export function base64_decode(encoded) {
// TODO replace with `Uint8Array.fromBase64(encoded)` when we require Node >= 25
// Using `Buffer` is faster than iterating
if (!BROWSER && globalThis.Buffer) {
const buffer = globalThis.Buffer.from(encoded, 'base64');
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/utils/regex.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
* @returns {string} escaped string
*/
export function escape_for_regexp(str) {
// TODO replace with `RegExp.escape(str)` when we require Node >= 24
return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
}
1 change: 1 addition & 0 deletions packages/kit/src/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function relative_pathname(from, to) {
export function matches_external_allowlist_entry(location, allowed) {
if (location === allowed) return true;

// TODO replace the try/catch with `URL.parse` when browser support allows (Chrome 126, Firefox 126, Safari 18)
try {
const allow = new URL(allowed);
const loc = new URL(location, allow);
Expand Down
Loading