Skip to content
Draft
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
10 changes: 0 additions & 10 deletions packages/kernel-platforms/src/capabilities/fs/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ import type { FsConfig } from './types.ts';

describe('fs browser capability', () => {
describe('capabilityFactory', () => {
it('existsSync returns false', () => {
const config: FsConfig = { rootDir: '/root', existsSync: true };
const capability = capabilityFactory(config);

// eslint-disable-next-line n/no-sync
expect(capability.existsSync?.('/path')).toBe(false);
});

it.each([
{
name: 'promises.readFile',
Expand All @@ -26,7 +18,6 @@ describe('fs browser capability', () => {
name: 'all operations',
config: {
rootDir: '/root',
existsSync: true,
promises: {
readFile: true,
access: true,
Expand All @@ -43,7 +34,6 @@ describe('fs browser capability', () => {
const config: FsConfig = { rootDir: '/root' };
const capability = capabilityFactory(config);

expect(capability).not.toHaveProperty('existsSync');
expect(capability).not.toHaveProperty('promises');
});
});
Expand Down
1 change: 0 additions & 1 deletion packages/kernel-platforms/src/capabilities/fs/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const notImplemented = (name: string): never => {
};

export const { configStruct, capabilityFactory } = makeFsSpecification({
makeExistsSync: () => () => false,
promises: {
makeReadFile: () => notImplemented('readFile'),
makeAccess: () => notImplemented('access'),
Expand Down
47 changes: 1 addition & 46 deletions packages/kernel-platforms/src/capabilities/fs/nodejs.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, lstatSync, Stats } from 'node:fs';
import { lstatSync, Stats } from 'node:fs';
import fs from 'node:fs/promises';
import { relative } from 'node:path';
import { describe, expect, it, vi, beforeEach } from 'vitest';
Expand All @@ -20,7 +20,6 @@ vi.mock('node:fs/promises', () => ({

// Mock fs
vi.mock('node:fs', () => ({
existsSync: vi.fn(),
lstatSync: vi.fn(),
}));

Expand Down Expand Up @@ -140,50 +139,6 @@ describe('fs nodejs capability', () => {
});

describe('capabilityFactory', () => {
describe('existsSync operation', () => {
it('returns true for existing file', () => {
vi.mocked(existsSync).mockReturnValue(true);

const config: FsConfig = { rootDir: '/root', existsSync: true };
const capability = capabilityFactory(config);

const result = capability.existsSync?.('/root/file.txt');
expect(existsSync).toHaveBeenCalledWith('/root/file.txt');
expect(result).toBe(true);
});

it.each([
{
name: 'outside root',
relativeReturn: '../../outside/file.txt',
isSymlink: false,
testPath: '/outside/file.txt',
expectedError: 'Path /outside/file.txt is outside allowed root /root',
},
{
name: 'symlink',
relativeReturn: '/root/file.txt',
isSymlink: true,
testPath: '/root/file.txt',
expectedError: 'Symlinks are prohibited: /root/file.txt',
},
])(
'throws error for path $name',
({ relativeReturn, isSymlink, testPath, expectedError }) => {
createMockRelative(relativeReturn);
createMockLstatSync(isSymlink);

const config: FsConfig = { rootDir: '/root', existsSync: true };
const capability = capabilityFactory(config);

expect(() => capability.existsSync?.(testPath)).toThrow(
expectedError,
);
expect(existsSync).not.toHaveBeenCalled();
},
);
});

describe.each([
{
operation: 'readFile',
Expand Down
3 changes: 1 addition & 2 deletions packages/kernel-platforms/src/capabilities/fs/nodejs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, lstatSync } from 'node:fs';
import { lstatSync } from 'node:fs';
import fs from 'node:fs/promises';
import { relative } from 'node:path';

Expand Down Expand Up @@ -54,7 +54,6 @@ const makeNodejsPathCaveat = (rootDir: string): SyncPathCaveat => {
};

export const { configStruct, capabilityFactory } = makeFsSpecification({
makeExistsSync: () => existsSync,
promises: {
makeReadFile: () => fs.readFile,
makeAccess: () => fs.access,
Expand Down
73 changes: 2 additions & 71 deletions packages/kernel-platforms/src/capabilities/fs/shared.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { describe, expect, it, vi } from 'vitest';

import {
makeCaveatedFsOperation,
makeCaveatedSyncFsOperation,
makeFsSpecification,
} from './shared.ts';
import type { ReadFile, Access, ExistsSync, SyncPathCaveat } from './types.ts';
import { makeCaveatedFsOperation, makeFsSpecification } from './shared.ts';
import type { ReadFile, Access, SyncPathCaveat } from './types.ts';

describe('makeCaveatedFsOperation', () => {
it('applies caveat before operation', async () => {
Expand Down Expand Up @@ -57,64 +53,14 @@ describe('makeCaveatedFsOperation', () => {
});
});

describe('makeCaveatedSyncFsOperation', () => {
it('applies caveat before operation', () => {
const mockOperation = vi.fn().mockReturnValue('result');
const mockCaveat = vi.fn().mockReturnValue(undefined);

const caveatedOperation = makeCaveatedSyncFsOperation(
mockOperation,
mockCaveat,
);

const result = caveatedOperation('/path', 'arg2', 'arg3');

expect(mockCaveat).toHaveBeenCalledWith('/path');
expect(mockOperation).toHaveBeenCalledWith('/path', 'arg2', 'arg3');
expect(result).toBe('result');
});

it('throws on caveat rejection', () => {
const mockOperation = vi.fn();
const mockCaveat = vi.fn().mockImplementation(() => {
throw new Error('Path not allowed');
});

const caveatedOperation = makeCaveatedSyncFsOperation(
mockOperation,
mockCaveat,
);

expect(() => caveatedOperation('/path')).toThrow('Path not allowed');
expect(mockCaveat).toHaveBeenCalledWith('/path');
expect(mockOperation).not.toHaveBeenCalled();
});

it('handles void operations', () => {
const mockOperation = vi.fn().mockReturnValue(undefined);
const mockCaveat = vi.fn().mockReturnValue(undefined);

const caveatedOperation = makeCaveatedSyncFsOperation(
mockOperation,
mockCaveat,
);

expect(caveatedOperation('/path')).toBeUndefined();
expect(mockCaveat).toHaveBeenCalledWith('/path');
expect(mockOperation).toHaveBeenCalledWith('/path');
});
});

describe('makeFsSpecification', () => {
const createMockSpecification = () => {
const mockReadFile: ReadFile = vi.fn();
const mockAccess: Access = vi.fn();
const mockExistsSync: ExistsSync = vi.fn();
const mockPathCaveat: SyncPathCaveat = vi.fn();

return {
specification: makeFsSpecification({
makeExistsSync: () => mockExistsSync,
promises: {
makeReadFile: () => mockReadFile,
makeAccess: () => mockAccess,
Expand All @@ -123,7 +69,6 @@ describe('makeFsSpecification', () => {
}),
mockReadFile,
mockAccess,
mockExistsSync,
mockPathCaveat,
};
};
Expand All @@ -135,21 +80,11 @@ describe('makeFsSpecification', () => {
expect(specification).toHaveProperty('capabilityFactory');
});

it('creates capability with existsSync', () => {
const { specification } = createMockSpecification();
const config = { rootDir: '/root', existsSync: true };
const capability = specification.capabilityFactory(config);

expect(capability).toHaveProperty('existsSync');
expect(capability).not.toHaveProperty('promises');
});

it('creates capability with promises.readFile', () => {
const { specification } = createMockSpecification();
const config = { rootDir: '/root', promises: { readFile: true } };
const capability = specification.capabilityFactory(config);

expect(capability).not.toHaveProperty('existsSync');
expect(capability).toHaveProperty('promises');
expect(capability.promises).toHaveProperty('readFile');
expect(capability.promises).not.toHaveProperty('access');
Expand All @@ -160,7 +95,6 @@ describe('makeFsSpecification', () => {
const config = { rootDir: '/root', promises: { access: true } };
const capability = specification.capabilityFactory(config);

expect(capability).not.toHaveProperty('existsSync');
expect(capability).toHaveProperty('promises');
expect(capability.promises).not.toHaveProperty('readFile');
expect(capability.promises).toHaveProperty('access');
Expand All @@ -170,15 +104,13 @@ describe('makeFsSpecification', () => {
const { specification } = createMockSpecification();
const config = {
rootDir: '/root',
existsSync: true,
promises: {
readFile: true,
access: true,
},
};
const capability = specification.capabilityFactory(config);

expect(capability).toHaveProperty('existsSync');
expect(capability).toHaveProperty('promises');
expect(capability.promises).toHaveProperty('readFile');
expect(capability.promises).toHaveProperty('access');
Expand All @@ -189,7 +121,6 @@ describe('makeFsSpecification', () => {
const config = { rootDir: '/root' };
const capability = specification.capabilityFactory(config);

expect(capability).not.toHaveProperty('existsSync');
expect(capability).not.toHaveProperty('promises');
});
});
41 changes: 2 additions & 39 deletions packages/kernel-platforms/src/capabilities/fs/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import type {
SyncPathCaveat,
ReadFile,
Access,
ExistsSync,
FsConfig,
FsCapability,
} from './types.ts';
import { fsConfigStruct } from './types.ts';
import { makeCapabilitySpecification } from '../../specification.ts';

/**
* Cross-platform FS operation wrapper with validation (async version)
* Cross-platform FS operation wrapper with validation
*
* @param operation - The underlying operation to wrap
* @param syncPathCaveat - The caveat to apply to path arguments
Expand All @@ -36,49 +35,21 @@ export const makeCaveatedFsOperation = <
}) as Operation;
};

/**
* Cross-platform synchronous FS operation wrapper with validation
*
* @param operation - The underlying synchronous operation to wrap
* @param syncPathCaveat - The caveat to apply to path arguments
* @returns The operation restricted by the provided caveat
*/
export const makeCaveatedSyncFsOperation = <
Operation extends (...args: never[]) => unknown,
>(
operation: Operation,
syncPathCaveat: SyncPathCaveat,
): Operation => {
return harden((...args: Parameters<Operation>) => {
try {
// Assuming first argument is always the path
syncPathCaveat(args[0] as unknown as PathLike);
} catch (cause) {
const message = cause instanceof Error ? cause.message : 'Caveat failed';
throw new Error(`fs.${operation.name}: ${message}`, { cause });
}
return operation(...args);
}) as Operation;
};

/* eslint-disable @typescript-eslint/explicit-function-return-type */
/**
* Cross-platform FS capability specification factory
*
* @param config - The configuration for the capability specification
* @param config.makeExistsSync - The factory returning an existsSync operation
* @param config.promises - Object containing promise-based operation factories
* @param config.promises.makeReadFile - The factory returning a read file operation
* @param config.promises.makeAccess - The factory returning an access operation
* @param config.makePathCaveat - Factory function to create path caveats
* @returns The capability specification
*/
export const makeFsSpecification = ({
makeExistsSync,
promises,
makePathCaveat,
}: {
makeExistsSync: () => ExistsSync;
promises: {
makeReadFile: () => ReadFile;
makeAccess: () => Access;
Expand All @@ -90,19 +61,11 @@ export const makeFsSpecification = ({
(config: FsConfig): FsCapability => {
// The construction of this capability left ad-hoc until additional
// requirements dictate additional structure.
const { rootDir, existsSync, promises: promisesConfig } = config;
const { rootDir, promises: promisesConfig } = config;
const caveat = makePathCaveat(rootDir);

const toExport: FsCapability = {};

if (existsSync) {
toExport.existsSync = makeCaveatedSyncFsOperation(
// eslint-disable-next-line n/no-sync
makeExistsSync(),
caveat,
);
}

if (promisesConfig) {
const promisesObj: FsCapability['promises'] = {};

Expand Down
Loading
Loading