Skip to content

Commit

Permalink
feat(volume): implement readv and writev (#946)
Browse files Browse the repository at this point in the history
* fix(volume): readBase throws error when given length is larger than buffer size

* test(volume): add tests for readSync

* feat(volume): add readv and readvSync

* feat(volume): add readv and writev

* feat(volume): add readv and writev to FileHandle

* fix: add methods to the list

* fix: support -1 in all read and write methods
  • Loading branch information
uhyo authored Sep 22, 2023
1 parent 125a996 commit 966e17e
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 17 deletions.
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

0 comments on commit 966e17e

Please sign in to comment.