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
79 changes: 76 additions & 3 deletions native/v8-runtime/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// The ICU major version that the bundled `icudtl.dat` was built for.
/// Update this constant (and the `icudtl.dat` file) when upgrading the V8
/// crate to a version that ships a different ICU.
///
/// To update:
/// 1. Check the new ICU version in the V8 crate's
/// `third_party/icu/source/common/unicode/uvernum.h` (U_ICU_VERSION_MAJOR_NUM).
/// 2. Download the matching full ICU data from:
/// https://github.com/unicode-org/icu/releases/download/release-{MAJOR}-{MINOR}/icu4c-{MAJOR}_{MINOR}-data-bin-l.zip
/// 3. Extract `icudt{MAJOR}l.dat`, rename to `icudtl.dat`, and place it
/// in this directory (`native/v8-runtime/icudtl.dat`).
/// 4. Update `BUNDLED_ICU_MAJOR_VERSION` below.
const BUNDLED_ICU_MAJOR_VERSION: u32 = 74;

fn cargo_home() -> PathBuf {
if let Some(home) = env::var_os("CARGO_HOME") {
return PathBuf::from(home);
Expand Down Expand Up @@ -34,11 +48,64 @@ fn read_v8_version(lock_path: &Path) -> String {
panic!("failed to locate v8 version in {}", lock_path.display());
}

fn find_v8_icu_data(v8_version: &str) -> PathBuf {
/// Read U_ICU_VERSION_MAJOR_NUM from the V8 crate's uvernum.h header.
fn read_v8_icu_major_version(v8_version: &str) -> Option<u32> {
let registry_src = cargo_home().join("registry").join("src");
let entries = fs::read_dir(&registry_src).ok()?;

for entry in entries.flatten() {
let header = entry
.path()
.join(format!("v8-{}", v8_version))
.join("third_party/icu/source/common/unicode/uvernum.h");
if let Ok(content) = fs::read_to_string(&header) {
for line in content.lines() {
if line.contains("U_ICU_VERSION_MAJOR_NUM") && !line.contains("ifndef") {
if let Some(num) = line.split_whitespace().last() {
return num.parse().ok();
}
}
}
}
}

None
}

fn find_v8_icu_data(v8_version: &str, manifest_dir: &Path) -> PathBuf {
// Prefer the full ICU data bundled in the repo. The V8 crate only ships a
// stripped-down flutter_desktop/icudtl.dat (~1.6 MB) that excludes locale
// data for NumberFormat, DateTimeFormat, and most non-English locales,
// causing "Internal error. Icu error." at runtime.
let bundled = manifest_dir.join("icudtl.dat");
if bundled.exists() {
// Verify the bundled data matches the V8 crate's ICU version.
if let Some(v8_icu_major) = read_v8_icu_major_version(v8_version) {
if v8_icu_major != BUNDLED_ICU_MAJOR_VERSION {
panic!(
"\n\n\
*** ICU version mismatch ***\n\
The V8 crate (v8-{v8}) uses ICU {v8_icu}, but the bundled icudtl.dat \
is for ICU {bundled}.\n\n\
To fix:\n \
1. Download: https://github.com/unicode-org/icu/releases/download/\
release-{v8_icu}-1/icu4c-{v8_icu}_1-data-bin-l.zip\n \
2. Extract the .dat file and save as native/v8-runtime/icudtl.dat\n \
3. Update BUNDLED_ICU_MAJOR_VERSION to {v8_icu} in build.rs\n\n",
v8 = v8_version,
v8_icu = v8_icu_major,
bundled = BUNDLED_ICU_MAJOR_VERSION,
);
}
}
return bundled;
}

// Fallback: search the V8 crate in the cargo registry.
let registry_src = cargo_home().join("registry").join("src");
let candidates = [
Path::new("third_party/icu/common/icudtl.dat"),
Path::new("third_party/icu/flutter_desktop/icudtl.dat"),
Path::new("third_party/icu/common/icudtl.dat"),
Path::new("third_party/icu/chromecast_video/icudtl.dat"),
];

Expand All @@ -57,6 +124,11 @@ fn find_v8_icu_data(v8_version: &str) -> PathBuf {
for relative in candidates {
let candidate = crate_root.join(relative);
if candidate.exists() {
println!(
"cargo:warning=Using stripped ICU data from V8 crate. \
Intl.NumberFormat/DateTimeFormat may fail for non-English locales. \
See native/v8-runtime/build.rs for instructions on bundling full ICU data."
);
return candidate;
}
}
Expand All @@ -77,9 +149,10 @@ fn main() {

println!("cargo:rerun-if-changed={}", lock_path.display());
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=icudtl.dat");

let v8_version = read_v8_version(&lock_path);
let icu_data = find_v8_icu_data(&v8_version);
let icu_data = find_v8_icu_data(&v8_version, &manifest_dir);
let dest_path = out_dir.join("icudtl.dat");

fs::copy(&icu_data, &dest_path).unwrap_or_else(|error| {
Expand Down
Binary file added native/v8-runtime/icudtl.dat
Binary file not shown.
2 changes: 1 addition & 1 deletion native/v8-runtime/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-v8-darwin-arm64",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"license": "Apache-2.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
Expand Down
2 changes: 1 addition & 1 deletion native/v8-runtime/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-v8-darwin-x64",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"license": "Apache-2.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
Expand Down
2 changes: 1 addition & 1 deletion native/v8-runtime/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-v8-linux-arm64-gnu",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"license": "Apache-2.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
Expand Down
2 changes: 1 addition & 1 deletion native/v8-runtime/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-v8-linux-x64-gnu",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"license": "Apache-2.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
Expand Down
2 changes: 1 addition & 1 deletion native/v8-runtime/npm/win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-v8-win32-x64",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"license": "Apache-2.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firestart-secure-exec-monorepo",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-core",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"type": "module",
"license": "Apache-2.0",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec-nodejs",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"type": "module",
"license": "Apache-2.0",
"main": "./dist/index.js",
Expand Down
14 changes: 5 additions & 9 deletions packages/nodejs/src/bridge-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4145,16 +4145,12 @@ async function maybeDecompressHttpBody(
}

function shouldEncodeHttpBodyAsBinary(
urlString: string,
headers: http.IncomingHttpHeaders,
_urlString: string,
_headers: http.IncomingHttpHeaders,
): boolean {
const contentType = headers["content-type"] || "";
const headerValue = Array.isArray(contentType) ? contentType.join(", ") : contentType;
return (
headerValue.includes("octet-stream") ||
headerValue.includes("gzip") ||
urlString.endsWith(".tgz")
);
// Always encode response bodies as base64 across the bridge to prevent
// binary corruption. The isolate decodes via Buffer.from(body, "base64").
return true;
}

/**
Expand Down
114 changes: 92 additions & 22 deletions packages/nodejs/src/bridge/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,17 @@ function encodeFetchBody(
body: string,
bodyEncoding: string | null,
): Uint8Array {
if (bodyEncoding === "base64" && typeof Buffer !== "undefined") {
return new Uint8Array(Buffer.from(body, "base64"));
if (bodyEncoding === "base64") {
if (typeof Buffer !== "undefined") {
return new Uint8Array(Buffer.from(body, "base64"));
}
// Fallback base64 decode via atob
const binary = atob(body);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(body);
Expand Down Expand Up @@ -504,16 +513,22 @@ export async function fetch(input: string | URL | Request, options: FetchOptions
await sendBytes(bytes);
}
} else {
// Node.js Readable stream
// Node.js Readable stream — pause/resume for backpressure
await new Promise<void>((resolve, reject) => {
const stream = rawBody as any;
let pending: Promise<void> = Promise.resolve();
stream.on("data", (data: any) => {
const bytes = typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array ? data : new Uint8Array(data);
sendBytes(bytes);
stream.pause();
pending = pending.then(() => sendBytes(bytes)).then(() => {
stream.resume();
});
});
stream.on("end", () => {
pending.then(() => resolve(), reject);
});
stream.on("end", () => resolve());
stream.on("error", (err: any) => reject(err));
});
}
Expand Down Expand Up @@ -635,16 +650,27 @@ export async function fetch(input: string | URL | Request, options: FetchOptions
},

async text(): Promise<string> {
if (bodyEncoding === "base64" && typeof Buffer !== "undefined") {
return Buffer.from(responseBody, "base64").toString("utf8");
if (bodyEncoding === "base64") {
if (typeof Buffer !== "undefined") {
return Buffer.from(responseBody, "base64").toString("utf8");
}
const binary = atob(responseBody);
return new TextDecoder().decode(Uint8Array.from(binary, c => c.charCodeAt(0)));
}
return responseBody;
},
async json(): Promise<unknown> {
const textBody =
bodyEncoding === "base64" && typeof Buffer !== "undefined"
? Buffer.from(responseBody, "base64").toString("utf8")
: responseBody;
let textBody: string;
if (bodyEncoding === "base64") {
if (typeof Buffer !== "undefined") {
textBody = Buffer.from(responseBody, "base64").toString("utf8");
} else {
const binary = atob(responseBody);
textBody = new TextDecoder().decode(Uint8Array.from(binary, c => c.charCodeAt(0)));
}
} else {
textBody = responseBody;
}
return JSON.parse(textBody || "{}");
},
async arrayBuffer(): Promise<ArrayBuffer> {
Expand Down Expand Up @@ -767,7 +793,7 @@ export class Request {

// Response class
export class Response {
private _body: string | null;
private _body: string | Uint8Array | null;
status: number;
statusText: string;
headers: Headers;
Expand All @@ -776,8 +802,19 @@ export class Response {
url: string;
redirected: boolean;

constructor(body?: string | null, init: { status?: number; statusText?: string; headers?: Record<string, string> } = {}) {
this._body = body || null;
constructor(body?: string | ReadableStream | ArrayBuffer | Uint8Array | null, init: { status?: number; statusText?: string; headers?: Record<string, string> } = {}) {
if (body === null || body === undefined) {
this._body = null;
} else if (typeof body === "string") {
this._body = body;
} else if (body instanceof Uint8Array) {
this._body = body;
} else if (body instanceof ArrayBuffer) {
this._body = new Uint8Array(body);
} else {
// ReadableStream — store reference, will be consumed lazily
this._body = body as any;
}
this.status = init.status || 200;
this.statusText = init.statusText || "OK";
this.headers = new Headers(init.headers);
Expand All @@ -787,34 +824,67 @@ export class Response {
this.redirected = false;
}

private async _bytes(): Promise<Uint8Array> {
const body = this._body;
if (body === null) return new Uint8Array(0);
if (body instanceof Uint8Array) return body;
if (typeof body === "string") return new TextEncoder().encode(body);
// ReadableStream
const reader = (body as any).getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value instanceof Uint8Array ? value : new Uint8Array(value));
}
const total = chunks.reduce((s: number, c: Uint8Array) => s + c.length, 0);
const result = new Uint8Array(total);
let off = 0;
for (const c of chunks) { result.set(c, off); off += c.length; }
// Cache for subsequent calls
(this as any)._body = result;
return result;
}

async text(): Promise<string> {
return String(this._body || "");
const bytes = await this._bytes();
return new TextDecoder().decode(bytes);
}

async json(): Promise<unknown> {
return JSON.parse(this._body || "{}");
return JSON.parse(await this.text());
}

async arrayBuffer(): Promise<ArrayBuffer> {
const bytes = await this._bytes();
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
}

async blob(): Promise<Blob> {
const bytes = await this._bytes();
return new Blob([bytes as any], { type: this.headers.get("content-type") || "" });
}

get body(): { getReader(): { read(): Promise<{ done: boolean; value?: Uint8Array }> } } | null {
const bodyStr = this._body;
if (bodyStr === null) return null;
const self = this;
if (this._body === null) return null;
return {
getReader() {
let consumed = false;
return {
async read() {
if (consumed) return { done: true };
if (consumed) return { done: true as const };
consumed = true;
const encoder = new TextEncoder();
return { done: false, value: encoder.encode(bodyStr) };
const bytes = await self._bytes();
return { done: false as const, value: bytes };
},
};
},
};
}

clone(): Response {
return new Response(this._body, { status: this.status, statusText: this.statusText });
return new Response(this._body as any, { status: this.status, statusText: this.statusText });
}

static error(): Response {
Expand Down
2 changes: 1 addition & 1 deletion packages/secure-exec/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestartorg/secure-exec",
"version": "0.2.2",
"version": "0.2.3-rc.7",
"type": "module",
"license": "Apache-2.0",
"main": "./dist/index.js",
Expand Down
Loading
Loading