Skip to content

Commit

Permalink
feat: 🎸 implement access() method
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 17, 2023
1 parent 6a2fa2d commit c72390b
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 16 deletions.
11 changes: 11 additions & 0 deletions src/consts/AMODE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Constants used in `access` system call, see [access(2)](http://man7.org/linux/man-pages/man2/faccessat.2.html).
export const enum AMODE {
/* Tests for the existence of the file. */
F_OK = 0,
/** Tests for Execute or Search permissions. */
X_OK = 1,
/** Tests for Write permission. */
W_OK = 2,
/** Tests for Read permission. */
R_OK = 4,
}
84 changes: 73 additions & 11 deletions src/fsa-to-node/FsaNodeFs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ import {
validateCallback,
validateFd,
} from '../node/util';
import { pathToLocation } from './util';
import { pathToLocation, testDirectoryIsWritable } from './util';
import { ERRSTR, MODE } from '../node/constants';
import { strToEncoding } from '../encoding';
import { FsaToNodeConstants } from './constants';
import { bufferToEncoding } from '../volume';
import { FsaNodeFsOpenFile } from './FsaNodeFsOpenFile';
import { FsaNodeDirent } from './FsaNodeDirent';
import {FLAG} from '../consts/FLAG';
import {AMODE} from '../consts/AMODE';
import type { FsCallbackApi, FsPromisesApi } from '../node/types';
import type * as misc from '../node/types/misc';
import type * as opts from '../node/types/options';
Expand Down Expand Up @@ -86,6 +87,18 @@ export class FsaNodeFs implements FsCallbackApi {
return file;
}

private async getFileOrDir(path: string[], name: string, funcName?: string, create?: boolean): Promise<fsa.IFileSystemFileHandle | fsa.IFileSystemDirectoryHandle> {
const dir = await this.getDir(path, false, funcName);
try {
const file = await dir.getFileHandle(name);
return file;
} catch (error) {
if (error && typeof error === 'object' && error.name === 'TypeMismatchError')
return await dir.getDirectoryHandle(name);
throw error;
}
}

private async getFileByFd(fd: number, funcName?: string): Promise<FsaNodeFsOpenFile> {
if (!isFd(fd)) throw TypeError(ERRSTR.FD);
const file = this.fds.get(fd);
Expand All @@ -108,6 +121,9 @@ export class FsaNodeFs implements FsCallbackApi {
return await dir.getFileHandle(name, { create: true });
}


// ------------------------------------------------------------ FsCallbackApi

public readonly open: FsCallbackApi['open'] = (
path: misc.PathLike,
flags: misc.TFlags,
Expand Down Expand Up @@ -289,15 +305,58 @@ export class FsaNodeFs implements FsCallbackApi {
throw new Error('Not implemented');
}

exists(path: misc.PathLike, callback: (exists: boolean) => void): void {
throw new Error('Not implemented');
}
public readonly exists: FsCallbackApi['exists'] = (path: misc.PathLike, callback: (exists: boolean) => void): void => {
const filename = pathToFilename(path);
if (typeof callback !== 'function') throw Error(ERRSTR.CB);
const [folder, name] = pathToLocation(filename);
(async () => {
// const stats = await new Promise();
})().then(() => callback(true), () => callback(false));
};

access(path: misc.PathLike, callback: misc.TCallback<void>);
access(path: misc.PathLike, mode: number, callback: misc.TCallback<void>);
access(path: misc.PathLike, a: misc.TCallback<void> | number, b?: misc.TCallback<void>) {
throw new Error('Not implemented');
}
public readonly access: FsCallbackApi['access'] = (path: misc.PathLike, a: misc.TCallback<void> | number, b?: misc.TCallback<void>) => {
let mode: number = AMODE.F_OK;
let callback: misc.TCallback<void>;
if (typeof a !== 'function') {
mode = a | 0; // cast to number
callback = validateCallback(b);
} else {
callback = a;
}
const filename = pathToFilename(path);
const [folder, name] = pathToLocation(filename);
(async () => {
const node = await this.getFileOrDir(folder, name, 'access');
const checkIfCanExecute = mode & AMODE.X_OK;
if (checkIfCanExecute) throw createError('EACCESS', 'access', filename);
const checkIfCanWrite = mode & AMODE.W_OK;
switch (node.kind) {
case 'file': {
if (checkIfCanWrite) {
try {
const file = node as fsa.IFileSystemFileHandle;
const writable = await file.createWritable();
await writable.close();
} catch {
throw createError('EACCESS', 'access', filename);
}
}
break;
}
case 'directory': {
if (checkIfCanWrite) {
const dir = node as fsa.IFileSystemDirectoryHandle;
const canWrite = await testDirectoryIsWritable(dir);
if (!canWrite) throw createError('EACCESS', 'access', filename);
}
break;
}
default: {
throw createError('EACCESS', 'access', filename);
}
}
})().then(() => callback(null), error => callback(error));
};

public readonly appendFile: FsCallbackApi['appendFile'] = (id: misc.TFileId, data: misc.TData, a, b?) => {
const [opts, callback] = getAppendFileOptsAndCb(a, b);
Expand All @@ -307,8 +366,11 @@ export class FsaNodeFs implements FsCallbackApi {
(async () => {
const blob = await file.getFile();
const writable = await file.createWritable({ keepExistingData: true });
await writable.seek(blob.size);
await writable.write(buffer);
await writable.write({
type: 'write',
data: buffer,
position: blob.size,
});
await writable.close();
})(),
)
Expand Down
13 changes: 10 additions & 3 deletions src/fsa-to-node/FsaNodeFsOpenFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import {FLAG} from '../consts/FLAG';
import type * as fsa from '../fsa/types';
import type * as misc from '../node/types/misc';

/**
* Represents an open file. Stores additional metadata about the open file, such
* as the seek position.
*/
export class FsaNodeFsOpenFile {
protected seek: number = 0;

Expand All @@ -10,14 +15,16 @@ export class FsaNodeFsOpenFile {
* with which flags the file was opened. On subsequent writes we want to
* append to the file.
*/
protected keepExistingData: boolean = false;
protected keepExistingData: boolean;

public constructor(
public readonly fd: number,
public readonly mode: misc.TMode,
public readonly createMode: misc.TMode,
public readonly flags: number,
public readonly file: fsa.IFileSystemFileHandle,
) {}
) {
this.keepExistingData = !!(flags & FLAG.O_APPEND);
}

public async close(): Promise<void> {}

Expand Down
94 changes: 92 additions & 2 deletions src/fsa-to-node/__tests__/FsaNodeFs.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IFsWithVolume, NestedDirectoryJSON, memfs } from '../..';
import {AMODE} from '../../consts/AMODE';
import { nodeToFsa } from '../../node-to-fsa';
import { IDirent } from '../../node/types/misc';
import { FsaNodeFs } from '../FsaNodeFs';

const setup = (json: NestedDirectoryJSON | null = null) => {
const setup = (json: NestedDirectoryJSON | null = null, mode: 'read' | 'readwrite' = 'readwrite') => {
const mfs = memfs({ mountpoint: json }) as IFsWithVolume;
const dir = nodeToFsa(mfs, '/mountpoint', { mode: 'readwrite', syncHandleAllowed: true });
const dir = nodeToFsa(mfs, '/mountpoint', { mode, syncHandleAllowed: true });
const fs = new FsaNodeFs(dir);
return { fs, mfs, dir };
};
Expand Down Expand Up @@ -351,3 +352,92 @@ describe('.write()', () => {
expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('abc');
});
});

describe('.exists()', () => {
test('can works for folders and files', async () => {
// const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
// const exists = async (path: string): Promise<boolean> => {
// return new Promise((resolve) => {
// fs.exists(path, (exists) => resolve(exists));
// });
// };
// expect(await exists('/folder')).toBe(true);

});
});

describe('.access()', () => {
describe('files', () => {
test('succeeds on file existence check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder/file', AMODE.F_OK);
});

test('succeeds on file "read" check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder/file', AMODE.R_OK);
});

test('succeeds on file "write" check, on writable file system', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder/file', AMODE.W_OK);
});

test('fails on file "write" check, on read-only file system', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }, 'read');
try {
await fs.promises.access('/folder/file', AMODE.W_OK);
throw new Error('should not be here')
} catch (error) {
expect(error.code).toBe('EACCESS');
}
});

test('fails on file "execute" check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
try {
await fs.promises.access('/folder/file', AMODE.X_OK);
throw new Error('should not be here')
} catch (error) {
expect(error.code).toBe('EACCESS');
}
});
});

describe('directories', () => {
test('succeeds on folder existence check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder', AMODE.F_OK);
});

test('succeeds on folder "read" check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder', AMODE.R_OK);
});

test('succeeds on folder "write" check, on writable file system', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
await fs.promises.access('/folder', AMODE.W_OK);
});

test('fails on folder "write" check, on read-only file system', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }, 'read');
try {
await fs.promises.access('/folder', AMODE.W_OK);
throw new Error('should not be here')
} catch (error) {
expect(error.code).toBe('EACCESS');
}
});

test('fails on folder "execute" check', async () => {
const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' });
try {
await fs.promises.access('/folder', AMODE.X_OK);
throw new Error('should not be here')
} catch (error) {
expect(error.code).toBe('EACCESS');
}
});
});
});
15 changes: 15 additions & 0 deletions src/fsa-to-node/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {IFileSystemDirectoryHandle} from '../fsa/types';
import { FsaToNodeConstants } from './constants';
import type { FsLocation } from './types';

Expand All @@ -9,3 +10,17 @@ export const pathToLocation = (path: string): FsLocation => {
const folder = path.slice(0, lastSlashIndex).split(FsaToNodeConstants.Separator);
return [folder, file];
};

export const testDirectoryIsWritable = async (dir: IFileSystemDirectoryHandle): Promise<boolean> => {
const testFileName = '__memfs_writable_test_file_' + Math.random().toString(36).slice(2) + Date.now();
try {
await dir.getFileHandle(testFileName, { create: true });
return true;
} catch {
return false;
} finally {
try {
await dir.removeEntry(testFileName);
} catch (e) {}
}
};

0 comments on commit c72390b

Please sign in to comment.