Skip to content

Commit 716c70c

Browse files
authored
[PHP-wasm] Use statfs from NODEFS in the Node version (#94)
## Motivation for the change, related issues This PR adds `statfs` support to the Emscripten root file system to enable PHP-wasm Node to get real filesystem stats for the `/` path. Currently, in PHP-wasm Node, PHP functions like `disk_total_space('/')` return the default hardcoded value for MEMFS instead of the actual disk space. This happens because Emscripten automatically detects the filesystem for a given path, and because the root path always uses the MEMFS filesystem, Emscripten will use the MEMFS `statfs` implementation. PHP-wasm Node includes two Emscripten filesystems, MEMFS and NODEFS. MEMFS doesn't have access to the operating system's filesystem, so it can only return a hardcoded value. NODEFS has access to the OS filesystem through the `fs` module, and specifically for `statfs` it uses `fs.statfsSync` to get real data. ## Implementation details This PR uses Emscripten's `onRuntimeInitialized` event to override the `statfs` implementation in the Emscripten root FS with the NODEFS implementation of `statfs`. It also defines the `FS.root.mount.opts.root` path as `.` to ensure the root node has a path value defined. Otherwise when we call `FS.statfs('/')` it would use the root node's root path which is `undefined` and this would throw an error because it would call `fs.statfsSync(undefined)` in the NODEFS implementation of `statfs`. I explored fixing this [issue in Emscripten](emscripten-core/emscripten#23912), but I wasn't able to find a working solution there, so I ended up patching it in Playground. ## Testing Instructions (or ideally a Blueprint) - CI
1 parent b38db76 commit 716c70c

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

packages/php-wasm/node/src/test/php.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@ import {
88
setPhpIniEntries,
99
SupportedPHPVersions,
1010
} from '@php-wasm/universal';
11-
import { existsSync, rmSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
11+
import {
12+
existsSync,
13+
rmSync,
14+
readFileSync,
15+
mkdirSync,
16+
writeFileSync,
17+
statfsSync,
18+
mkdtempSync,
19+
} from 'fs';
1220
import { createSpawnHandler, joinPaths, phpVar } from '@php-wasm/util';
1321
import { createNodeFsMountHandler } from '../lib/node-fs-mount';
22+
import { tmpdir } from 'os';
1423

1524
const testDirPath = '/__test987654321';
1625
const testFilePath = '/__test987654321.txt';
@@ -2142,4 +2151,48 @@ bar1
21422151
expect(response.text).toBe('bool(false)\n');
21432152
});
21442153
});
2154+
describe('Disk space', () => {
2155+
it('should return the correct total disk space', async () => {
2156+
const response = await php.run({
2157+
code: `<?php echo disk_total_space('/');`,
2158+
});
2159+
const expectedStatfs = statfsSync('/');
2160+
const expectedTotalDiskSpace =
2161+
expectedStatfs.blocks * expectedStatfs.bsize;
2162+
expect(response.text).toBe(expectedTotalDiskSpace.toString());
2163+
});
2164+
2165+
it('should return the correct free disk space', async () => {
2166+
const response = await php.run({
2167+
code: `<?php echo json_encode(disk_free_space('/'));`,
2168+
});
2169+
const expectedStatfs = statfsSync('/');
2170+
const expectedFreeDiskSpace =
2171+
expectedStatfs.bavail * expectedStatfs.bsize;
2172+
expect(response.text).toBe(expectedFreeDiskSpace.toString());
2173+
});
2174+
2175+
it('should return a hardcoded value from MEMFS for a file created in MEMFS', async () => {
2176+
php.writeFile('/test.txt', new Uint8Array(1024));
2177+
const response = await php.run({
2178+
code: `<?php echo json_encode(disk_total_space('/test.txt'));`,
2179+
});
2180+
expect(response.text).toBe('4096000000');
2181+
});
2182+
2183+
it('should return the correct total disk space when passing a subdirectory', async () => {
2184+
const tempDir = mkdtempSync(joinPaths(tmpdir(), 'php-wasm-test-'));
2185+
const filePath = joinPaths(tempDir, 'test.txt');
2186+
writeFileSync(filePath, new Uint8Array(1024));
2187+
php.mount('/tmp', createNodeFsMountHandler(tempDir));
2188+
2189+
const response = await php.run({
2190+
code: `<?php echo json_encode(disk_total_space('/tmp'));`,
2191+
});
2192+
const expectedStatfs = statfsSync('/');
2193+
const expectedTotalDiskSpace =
2194+
expectedStatfs.blocks * expectedStatfs.bsize;
2195+
expect(response.text).toBe(expectedTotalDiskSpace.toString());
2196+
});
2197+
});
21452198
});

packages/php-wasm/universal/src/lib/load-php-runtime.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,50 @@ export async function loadPHPRuntime(
146146
...phpModuleArgs,
147147
noInitialRun: true,
148148
onRuntimeInitialized() {
149+
/**
150+
* Emscripten automatically detects the filesystem for a given path,
151+
* and because the root path always uses the MEMFS filesystem, `statfs`
152+
* will return the default hardcoded value for MEMFS instead of the
153+
* actual disk space.
154+
*
155+
* To ensure `statfs` works in the Node version of PHP-WASM,
156+
* we need to add `statfs` from NODEFS to the root FS.
157+
* Otherwise, `statfs` is undefined in the root FS and the NODEFS
158+
* implementation wouldn't be used for paths that exist in MEMFS.
159+
*
160+
* The only place `statfs` is used in PHP are the `disk_total_space`
161+
* and `disk_free_space` functions.
162+
* Both functions return the disk space for a given disk partition.
163+
* If a subdirectory is passed, the function will return the disk space
164+
* for its partition.
165+
*/
166+
if (currentJsRuntime === 'NODE') {
167+
PHPRuntime.FS.root.node_ops = {
168+
...PHPRuntime.FS.root.node_ops,
169+
statfs: PHPRuntime.FS.filesystems.NODEFS.node_ops.statfs,
170+
};
171+
/**
172+
* By default FS.root node value of `mount.opts.root` is `undefined`.
173+
* As a result `FS.lookupPath` will return a node with a `undefined`
174+
* `mount.opts.root` path when looking up the `/` path using `FS.lookupPath`.
175+
*
176+
* The `NODEFS.realPath` function works with `undefined` because it uses
177+
* `path.join` to build the path and for the `[undefined]` it will
178+
* return the `.` path.
179+
*
180+
* Because the `node.mount.opts.root` path is `undefined`,
181+
* `fs.statfsSync` will throw an error when trying to get the
182+
* disk space for an undefined path.
183+
* For the `/` path to correctly resolve, we must set the
184+
* `mount.opts.root` path to the current working directory.
185+
*
186+
* We chose the current working directory over `/` because
187+
* NODERAWFS defines the root path as `.`.
188+
* Emscripten reference to setting the root path in NODERAWFS:
189+
* https://github.com/emscripten-core/emscripten/pull/19400/files#diff-456b6256111c90ca5e6bdb583ab87108cd51cbbefc812c4785ea315c0728b3a8R11
190+
*/
191+
PHPRuntime.FS.root.mount.opts.root = '.';
192+
}
149193
if (phpModuleArgs.onRuntimeInitialized) {
150194
phpModuleArgs.onRuntimeInitialized();
151195
}

packages/php-wasm/universal/src/lib/php-request-handler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ export class PHPRequestHandler {
219219
});
220220
}
221221

222+
/**
223+
* By default, config.cookieStore is undefined, so we use the
224+
* HttpCookieStore implementation, otherwise we use the one
225+
* provided in the config.
226+
*
227+
* By explicitly checking for `undefined` we allow the user to pass
228+
* `null` as config.cookieStore and disable the cookie store.
229+
*/
222230
this.#cookieStore =
223231
config.cookieStore === undefined
224232
? new HttpCookieStore()

0 commit comments

Comments
 (0)