Skip to content
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
59 changes: 57 additions & 2 deletions abi/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@
{
"kind": "func",
"name": "kernel_fpathconf",
"signature": "(i32,i32) -> (i64)"
"signature": "(i32,i32,i32) -> (i32)"
},
{
"kind": "func",
Expand Down Expand Up @@ -1054,7 +1054,7 @@
{
"kind": "func",
"name": "kernel_pathconf",
"signature": "(i32,i32,i32) -> (i64)"
"signature": "(i32,i32,i32,i32) -> (i32)"
},
{
"kind": "func",
Expand Down Expand Up @@ -2988,6 +2988,32 @@
"size": 16
}
},
"pathconf_names": {
"ALLOC_SIZE_MIN": 18,
"ASYNC_IO": 10,
"CHOWN_RESTRICTED": 6,
"FALLOC": 21,
"FILESIZEBITS": 13,
"LINK_MAX": 0,
"MAX_CANON": 1,
"MAX_INPUT": 2,
"NAME_MAX": 3,
"NO_TRUNC": 7,
"PATH_MAX": 4,
"PIPE_BUF": 5,
"POSIX2_SYMLINKS": 20,
"PRIO_IO": 11,
"REC_INCR_XFER_SIZE": 14,
"REC_MAX_XFER_SIZE": 15,
"REC_MIN_XFER_SIZE": 16,
"REC_XFER_ALIGN": 17,
"SOCK_MAXBUF": 12,
"SYMLINK_MAX": 19,
"SYNC_IO": 9,
"TEXTDOMAIN_MAX": 22,
"TIMESTAMP_RESOLUTION": 23,
"VDISABLE": 8
},
"process_expected_globals": [
"__channel_base",
"__tls_base"
Expand Down Expand Up @@ -3168,6 +3194,35 @@
}
}
],
"112": [
{
"argIndex": 0,
"direction": "in",
"size": {
"type": "cstring"
}
},
{
"argIndex": 2,
"direction": "out",
"required": true,
"size": {
"size": 8,
"type": "fixed"
}
}
],
"113": [
{
"argIndex": 2,
"direction": "out",
"required": true,
"size": {
"size": 8,
"type": "fixed"
}
}
],
"114": [
{
"argIndex": 1,
Expand Down
73 changes: 73 additions & 0 deletions apps/browser-demos/test/fixtures/opfs-pathconf-client-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { PATHCONF_NAMES } from "../../../../host/src/generated/abi";
import { OpfsFileSystem } from "../../../../host/src/vfs/opfs";

const O_RDWR = 0x0002;
const O_CREAT = 0x0040;
const O_TRUNC = 0x0200;

function errorName(action: () => unknown): string | null {
try {
action();
return null;
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}

self.onmessage = (
event: MessageEvent<{ buffer: SharedArrayBuffer; path: string }>,
) => {
const { buffer, path } = event.data;
const fs = OpfsFileSystem.create(buffer);
let fd = -1;

try {
fd = fs.open(path, O_CREAT | O_TRUNC | O_RDWR, 0o600);
const result = {
type: "result",
// Keep the synchronous access handle open while pathname queries run:
// Chromium permits FileSystemFileHandle.getFile() here, but rejects a
// second createSyncAccessHandle() for the same file.
nameMax: fs.pathconf(path, PATHCONF_NAMES.NAME_MAX),
pathMax: fs.fpathconf(fd, PATHCONF_NAMES.PATH_MAX),
asyncIo: fs.fpathconf(fd, PATHCONF_NAMES.ASYNC_IO),
symlinks: fs.pathconf(path, PATHCONF_NAMES.POSIX2_SYMLINKS),
timestampResolution: fs.pathconf(
path,
PATHCONF_NAMES.TIMESTAMP_RESOLUTION,
),
};

const closedFd = fd;
fs.close(closedFd);
fd = -1;
const closedHandleError = errorName(() =>
fs.fpathconf(closedFd, PATHCONF_NAMES.NAME_MAX),
);
fs.unlink(path);
const missingPathError = errorName(() =>
fs.pathconf(path, PATHCONF_NAMES.NAME_MAX),
);

self.postMessage({ ...result, closedHandleError, missingPathError });
} catch (error) {
if (fd >= 0) {
try {
fs.close(fd);
} catch {
// Preserve the original failure.
}
}
try {
fs.unlink(path);
} catch {
// Preserve the original failure.
}
self.postMessage({
type: "error",
error: error instanceof Error ? error.message : String(error),
});
} finally {
self.close();
}
};
110 changes: 110 additions & 0 deletions apps/browser-demos/test/opfs-pathconf.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { expect, test } from "@playwright/test";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const proxyWorkerPath = resolve(
__dirname,
"../../../host/src/vfs/opfs-worker.ts",
);
const clientWorkerPath = resolve(
__dirname,
"fixtures/opfs-pathconf-client-worker.ts",
);

test("OPFS reports path configuration from live paths and handles", async ({
page,
baseURL,
browserName,
}) => {
test.skip(
browserName !== "chromium",
"OPFS sync access handles are Chromium-only here",
);
expect(baseURL).toBeTruthy();

const proxyWorkerUrl = new URL(`/@fs/${proxyWorkerPath}`, baseURL).href;
const clientWorkerUrl = new URL(`/@fs/${clientWorkerPath}`, baseURL).href;
await page.goto(new URL("/trap-signal-test.html", baseURL).href);

const result = await page.evaluate(
async ({ proxyWorkerUrl, clientWorkerUrl }) => {
const buffer = new SharedArrayBuffer(4 * 1024 * 1024);
const proxy = new Worker(proxyWorkerUrl, { type: "module" });
const client = new Worker(clientWorkerUrl, { type: "module" });

const receive = <T>(worker: Worker, expectedType: string): Promise<T> =>
new Promise((resolvePromise, reject) => {
const timeout = setTimeout(
() => reject(new Error(`timed out waiting for ${expectedType}`)),
15_000,
);
worker.addEventListener(
"message",
(event) => {
if (event.data?.type !== expectedType) {
if (event.data?.type === "error") {
clearTimeout(timeout);
reject(new Error(event.data.error));
}
return;
}
clearTimeout(timeout);
resolvePromise(event.data as T);
},
{ once: false },
);
worker.addEventListener(
"error",
(event) => {
clearTimeout(timeout);
reject(
new Error(
`${expectedType}: ${event.message || "worker module failed to load"} ` +
`(${event.filename}:${event.lineno}:${event.colno})`,
),
);
},
{ once: true },
);
});

try {
const ready = receive<{ type: "ready" }>(proxy, "ready");
proxy.postMessage({ type: "init", buffer });
await ready;

const pending = receive<{
type: "result";
nameMax: number;
pathMax: number;
asyncIo: number;
symlinks: null;
timestampResolution: null;
closedHandleError: string;
missingPathError: string;
}>(client, "result");
client.postMessage({
buffer,
path: `/kandelo-opfs-pathconf-${crypto.randomUUID()}`,
});
return await pending;
} finally {
client.terminate();
proxy.terminate();
}
},
{ proxyWorkerUrl, clientWorkerUrl },
);

expect(result).toEqual({
type: "result",
nameMax: 255,
pathMax: 4096,
asyncIo: 1,
symlinks: null,
timestampResolution: null,
closedHandleError: "EBADF",
missingPathError: "ENOENT",
});
});
52 changes: 52 additions & 0 deletions apps/browser-demos/test/pathconf.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const programPath = resolve(__dirname, "../../../examples/pathconf_test.wasm");

test("pathconf values, errno, and pointer validation work in Chromium", async ({
page,
baseURL,
browserName,
}) => {
test.skip(
browserName !== "chromium",
"the aggregate browser gate uses Chromium",
);
expect(baseURL).toBeTruthy();

const runtimeErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") {
runtimeErrors.push(`console: ${message.text()}`);
}
});
page.on("pageerror", (error) => {
runtimeErrors.push(`pageerror: ${error.message}`);
});

await page.goto(new URL("/pages/test-runner/", baseURL).href);
await page.waitForFunction(() => (window as any).__testRunnerReady === true);

const programUrl = new URL(`/@fs/${programPath}`, baseURL).href;
const result = await page.evaluate(
async ({ programUrl }) => {
const response = await fetch(programUrl);
if (!response.ok) {
throw new Error(`program fetch failed: ${response.status}`);
}
return (window as any).__runTest(
await response.arrayBuffer(),
["pathconf-test"],
15_000,
);
},
{ programUrl },
);

expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toContain("PATHCONF_PASS");
expect(result.stderr).toBe("");
expect(runtimeErrors).toEqual([]);
});
6 changes: 6 additions & 0 deletions crates/kernel/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ pub trait HostIO {
fn host_statfs(&mut self, _path: &[u8]) -> Result<WasmStatfs, Errno> {
Err(Errno::ENOSYS)
}
fn host_pathconf(&mut self, _path: &[u8], _name: i32) -> Result<Option<i64>, Errno> {
Err(Errno::ENOSYS)
}
fn host_fpathconf(&mut self, _handle: i64, _name: i32) -> Result<Option<i64>, Errno> {
Err(Errno::ENOSYS)
}
fn host_mkdir(&mut self, path: &[u8], mode: u32) -> Result<(), Errno>;
fn host_rmdir(&mut self, path: &[u8]) -> Result<(), Errno>;
fn host_unlink(&mut self, path: &[u8]) -> Result<(), Errno>;
Expand Down
Loading