Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(volume): implement readv and writev #946

Merged
merged 8 commits into from
Sep 22, 2023
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
50 changes: 48 additions & 2 deletions src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ describe('Promises API', () => {
});
it('Read data from an existing file', async () => {
const fileHandle = await promises.open('/foo', 'r+');
const buff = Buffer.from('foo');
const { bytesRead, buffer } = await fileHandle.read(buff, 0, 42, 0);
const buff = Buffer.from('foofoo');
const { bytesRead, buffer } = await fileHandle.read(buff, 0, 6, 0);
expect(bytesRead).toEqual(3);
expect(buffer).toBe(buff);
await fileHandle.close();
Expand All @@ -115,6 +115,30 @@ describe('Promises API', () => {
return expect(fileHandle.read(Buffer.from('foo'), 0, 42, 0)).rejects.toBeInstanceOf(Error);
});
});
describe('readv(buffers, position)', () => {
const vol = new Volume();
const { promises } = vol;
vol.fromJSON({
'/foo': 'Hello, world!',
});
it('Read data from an existing file', async () => {
const fileHandle = await promises.open('/foo', 'r+');
const buf1 = Buffer.alloc(5);
const buf2 = Buffer.alloc(5);
const { bytesRead, buffers } = await fileHandle.readv([buf1, buf2], 0);
expect(bytesRead).toEqual(10);
expect(buffers).toEqual([buf1, buf2]);
expect(buf1.toString()).toEqual('Hello');
expect(buf2.toString()).toEqual(', wor');
await fileHandle.close();
});
it('Reject when the file handle was closed', async () => {
const fileHandle = await promises.open('/foo', 'r+');
await fileHandle.close();
return expect(fileHandle.readv([Buffer.alloc(10)], 0)).rejects.toBeInstanceOf(Error);
});
});

describe('readFile([options])', () => {
const vol = new Volume();
const { promises } = vol;
Expand Down Expand Up @@ -243,6 +267,28 @@ describe('Promises API', () => {
return expect(fileHandle.write(Buffer.from('foo'))).rejects.toBeInstanceOf(Error);
});
});
describe('writev(buffers[, position])', () => {
const vol = new Volume();
const { promises } = vol;
vol.fromJSON({
'/foo': 'Hello, world!',
});
it('Write data to an existing file', async () => {
const fileHandle = await promises.open('/foo', 'w');
const buf1 = Buffer.from('foo');
const buf2 = Buffer.from('bar');
const { bytesWritten, buffers } = await fileHandle.writev([buf1, buf2], 0);
expect(vol.readFileSync('/foo').toString()).toEqual('foobar');
expect(bytesWritten).toEqual(6);
expect(buffers).toEqual([buf1, buf2]);
await fileHandle.close();
});
it('Reject when the file handle was closed', async () => {
const fileHandle = await promises.open('/foo', 'w');
await fileHandle.close();
return expect(fileHandle.writev([Buffer.from('foo')], 0)).rejects.toBeInstanceOf(Error);
});
});
describe('writeFile(data[, options])', () => {
const vol = new Volume();
const { promises } = vol;
Expand Down
73 changes: 73 additions & 0 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,63 @@ describe('volume', () => {
expect(fn).toThrowError('EPERM');
});
});
describe('.readv(fd, buffers, position, callback)', () => {
it('Simple read', done => {
const vol = new Volume();
vol.writeFileSync('/test.txt', 'hello');
const fd = vol.openSync('/test.txt', 'r');

const buf1 = Buffer.alloc(2);
const buf2 = Buffer.alloc(2);
const buf3 = Buffer.alloc(2);
vol.readv(fd, [buf1, buf2, buf3], 0, (err, bytesRead, buffers) => {
expect(err).toBe(null);
expect(bytesRead).toBe(5);
expect(buffers).toEqual([buf1, buf2, buf3]);
expect(buf1.toString()).toBe('he');
expect(buf2.toString()).toBe('ll');
expect(buf3.toString()).toBe('o\0');
done();
});
});
it('Read from position', done => {
const vol = new Volume();
vol.writeFileSync('/test.txt', 'hello');
const fd = vol.openSync('/test.txt', 'r');

const buf1 = Buffer.alloc(2);
const buf2 = Buffer.alloc(2);
const buf3 = Buffer.alloc(2, 0);
vol.readv(fd, [buf1, buf2, buf3], 1, (err, bytesRead, buffers) => {
expect(err).toBe(null);
expect(bytesRead).toBe(4);
expect(buffers).toEqual([buf1, buf2, buf3]);
expect(buf1.toString()).toBe('el');
expect(buf2.toString()).toBe('lo');
expect(buf3.toString()).toBe('\0\0');
done();
});
});
it('Read from current position', done => {
const vol = new Volume();
vol.writeFileSync('/test.txt', 'hello, world!');
const fd = vol.openSync('/test.txt', 'r');
vol.readSync(fd, Buffer.alloc(3), 0, 3, null);

const buf1 = Buffer.alloc(2);
const buf2 = Buffer.alloc(2);
const buf3 = Buffer.alloc(2);
vol.readv(fd, [buf1, buf2, buf3], (err, bytesRead, buffers) => {
expect(err).toBe(null);
expect(bytesRead).toBe(6);
expect(buffers).toEqual([buf1, buf2, buf3]);
expect(buf1.toString()).toBe('lo');
expect(buf2.toString()).toBe(', ');
expect(buf3.toString()).toBe('wo');
done();
});
});
});
describe('.readFileSync(path[, options])', () => {
const vol = new Volume();
const data = 'trololo';
Expand Down Expand Up @@ -624,6 +681,22 @@ describe('volume', () => {
});
});
});
describe('.writev(fd, buffers, position, callback)', () => {
it('Simple write to a file descriptor', done => {
const vol = new Volume();
const fd = vol.openSync('/test.txt', 'w+');
const data1 = 'Hello';
const data2 = ', ';
const data3 = 'world!';
vol.writev(fd, [Buffer.from(data1), Buffer.from(data2), Buffer.from(data3)], 0, (err, bytes) => {
expect(err).toBe(null);
expect(bytes).toBe(data1.length + data2.length + data3.length);
vol.closeSync(fd);
expect(vol.readFileSync('/test.txt', 'utf8')).toBe(data1 + data2 + data3);
done();
});
});
});
describe('.writeFile(path, data[, options], callback)', () => {
const vol = new Volume();
const data = 'asdfasidofjasdf';
Expand Down
33 changes: 30 additions & 3 deletions src/__tests__/volume/readSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,35 @@ describe('.readSync(fd, buffer, offset, length, position)', () => {
expect(bytes).toBe(3);
expect(buf.equals(Buffer.from('345'))).toBe(true);
});
xit('Read more than buffer space', () => {});
xit('Read over file boundary', () => {});
xit('Read multiple times, caret position should adjust', () => {});
it('Attempt to read more than buffer space should throw ERR_OUT_OF_RANGE', () => {
const vol = create({ '/test.txt': '01234567' });
const buf = Buffer.alloc(3, 0);
const fn = () => vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 10, 3);
expect(fn).toThrow('ERR_OUT_OF_RANGE');
});
it('Read over file boundary', () => {
const vol = create({ '/test.txt': '01234567' });
const buf = Buffer.alloc(3, 0);
const bytes = vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 3, 6);
expect(bytes).toBe(2);
expect(buf.equals(Buffer.from('67\0'))).toBe(true);
});
it('Read multiple times, caret position should adjust', () => {
const vol = create({ '/test.txt': '01234567' });
const buf = Buffer.alloc(3, 0);
const fd = vol.openSync('/test.txt', 'r');
let bytes = vol.readSync(fd, buf, 0, 3, null);
expect(bytes).toBe(3);
expect(buf.equals(Buffer.from('012'))).toBe(true);
bytes = vol.readSync(fd, buf, 0, 3, null);
expect(bytes).toBe(3);
expect(buf.equals(Buffer.from('345'))).toBe(true);
bytes = vol.readSync(fd, buf, 0, 3, null);
expect(bytes).toBe(2);
expect(buf.equals(Buffer.from('675'))).toBe(true);
bytes = vol.readSync(fd, buf, 0, 3, null);
expect(bytes).toBe(0);
expect(buf.equals(Buffer.from('675'))).toBe(true);
});
xit('Negative tests', () => {});
});
18 changes: 18 additions & 0 deletions src/node/FileHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class FileHandle implements IFileHandle {
return promisify(this.fs, 'read', bytesRead => ({ bytesRead, buffer }))(this.fd, buffer, offset, length, position);
}

readv(buffers: ArrayBufferView[], position?: number | null | undefined): Promise<TFileHandleReadvResult> {
return promisify(this.fs, 'readv', bytesRead => ({ bytesRead, buffers }))(this.fd, buffers, position);
}

readFile(options?: opts.IReadFileOptions | string): Promise<TDataOut> {
return promisify(this.fs, 'readFile')(this.fd, options);
}
Expand Down Expand Up @@ -72,6 +76,10 @@ export class FileHandle implements IFileHandle {
);
}

writev(buffers: ArrayBufferView[], position?: number | null | undefined): Promise<TFileHandleWritevResult> {
return promisify(this.fs, 'writev', bytesWritten => ({ bytesWritten, buffers }))(this.fd, buffers, position);
}

writeFile(data: TData, options?: opts.IWriteFileOptions): Promise<void> {
return promisify(this.fs, 'writeFile')(this.fd, data, options);
}
Expand All @@ -86,3 +94,13 @@ export interface TFileHandleWriteResult {
bytesWritten: number;
buffer: Buffer | Uint8Array;
}

export interface TFileHandleReadvResult {
bytesRead: number;
buffers: ArrayBufferView[];
}

export interface TFileHandleWritevResult {
bytesWritten: number;
buffers: ArrayBufferView[];
}
2 changes: 2 additions & 0 deletions src/node/lists/fsCallbackApiList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
'mkdtemp',
'open',
'read',
'readv',
'readdir',
'readFile',
'readlink',
Expand All @@ -41,5 +42,6 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
'watch',
'watchFile',
'write',
'writev',
'writeFile',
];
3 changes: 2 additions & 1 deletion src/node/lists/fsSynchronousApiList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const fsSynchronousApiList: Array<keyof FsSynchronousApi> = [
'readFileSync',
'readlinkSync',
'readSync',
'readvSync',
'realpathSync',
'renameSync',
'rmdirSync',
Expand All @@ -37,9 +38,9 @@ export const fsSynchronousApiList: Array<keyof FsSynchronousApi> = [
'utimesSync',
'writeFileSync',
'writeSync',
'writevSync',

// 'cpSync',
// 'lutimesSync',
// 'statfsSync',
// 'writevSync',
];
12 changes: 12 additions & 0 deletions src/node/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface IFileHandle {
close(): Promise<void>;
datasync(): Promise<void>;
read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise<TFileHandleReadResult>;
readv(buffers: ArrayBufferView[], position?: number | null): Promise<TFileHandleReadvResult>;
readFile(options?: IReadFileOptions | string): Promise<TDataOut>;
stat(options?: IStatOptions): Promise<IStats>;
truncate(len?: number): Promise<void>;
Expand All @@ -140,6 +141,7 @@ export interface IFileHandle {
length?: number,
position?: number,
): Promise<TFileHandleWriteResult>;
writev(buffers: ArrayBufferView[], position?: number | null): Promise<TFileHandleWritevResult>;
writeFile(data: TData, options?: IWriteFileOptions): Promise<void>;
}

Expand All @@ -155,4 +157,14 @@ export interface TFileHandleWriteResult {
buffer: Buffer | Uint8Array;
}

export interface TFileHandleReadvResult {
bytesRead: number;
buffers: ArrayBufferView[];
}

export interface TFileHandleWritevResult {
bytesWritten: number;
buffers: ArrayBufferView[];
}

export type AssertCallback<T> = T extends () => void ? T : never;
3 changes: 3 additions & 0 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const EISDIR = 'EISDIR';
const ENOTEMPTY = 'ENOTEMPTY';
const ENOSYS = 'ENOSYS';
const ERR_FS_EISDIR = 'ERR_FS_EISDIR';
const ERR_OUT_OF_RANGE = 'ERR_OUT_OF_RANGE';

function formatError(errorCode: string, func = '', path = '', path2 = '') {
let pathFormatted = '';
Expand Down Expand Up @@ -129,6 +130,8 @@ function formatError(errorCode: string, func = '', path = '', path2 = '') {
return `ENOSYS: function not implemented, ${func}${pathFormatted}`;
case ERR_FS_EISDIR:
return `[ERR_FS_EISDIR]: Path is a directory: ${func} returned EISDIR (is a directory) ${path}`;
case ERR_OUT_OF_RANGE:
return `[ERR_OUT_OF_RANGE]: value out of range, ${func}${pathFormatted}`;
default:
return `${errorCode}: error occurred, ${func}${pathFormatted}`;
}
Expand Down
Loading