diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 63694e109..7053715c7 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -6,6 +6,9 @@ import { Volume, filenameToSteps, StatWatcher } from '../volume'; import hasBigInt from './hasBigInt'; import { tryGetChild, tryGetChildNode } from './util'; import { genRndStr6 } from '../node/util'; +import { constants } from '../constants'; + +const { O_RDWR, O_SYMLINK } = constants; describe('volume', () => { describe('filenameToSteps(filename): string[]', () => { @@ -484,13 +487,26 @@ describe('volume', () => { }); }); describe('.read(fd, buffer, offset, length, position, callback)', () => { - xit('...', () => {}); + const vol = new Volume(); + const data = 'trololo'; + const fileNode = (vol as any).createLink(vol.root, 'text.txt').getNode(); + fileNode.setString(data); + vol.symlinkSync('/text.txt', '/link.txt'); + + it('Attempt to read from a symlink should throw EPERM', () => { + const fd = vol.openSync('/link.txt', O_SYMLINK); + expect(vol.fstatSync(fd).isSymbolicLink()).toBe(true); + const buf = Buffer.alloc(10); + const fn = () => vol.readSync(fd, buf, 0, 10, 0); + expect(fn).toThrowError('EPERM'); + }); }); describe('.readFileSync(path[, options])', () => { const vol = new Volume(); const data = 'trololo'; const fileNode = (vol as any).createLink(vol.root, 'text.txt').getNode(); fileNode.setString(data); + it('Read file at root (/text.txt)', () => { const buf = vol.readFileSync('/text.txt'); const str = buf.toString(); @@ -584,6 +600,16 @@ describe('volume', () => { vol.closeSync(fd); expect(vol.readFileSync('/overwrite.txt', 'utf8')).toBe('mArmagedon'); }); + it('Attempt to write to a symlink should throw EBADF', () => { + const data = 'asdfasdf asdfasdf asdf'; + vol.writeFileSync('/file.txt', data); + vol.symlinkSync('/file.txt', '/link.txt'); + + const fd = vol.openSync('/link.txt', O_SYMLINK | O_RDWR); + expect(vol.fstatSync(fd).isSymbolicLink()).toBe(true); + const fn = () => vol.writeSync(fd, 'hello'); + expect(fn).toThrowError('EBADF'); + }); }); describe('.write(fd, buffer, offset, length, position, callback)', () => { it('Simple write to a file descriptor', done => { @@ -834,6 +860,8 @@ describe('volume', () => { const data = '(function(){})();'; dojo.getNode().setString(data); + vol.symlinkSync('/dojo.js', '/link.js'); + it('Returns basic file stats', () => { const fd = vol.openSync('/dojo.js', 'r'); const stats = vol.fstatSync(fd); @@ -855,6 +883,21 @@ describe('volume', () => { expect(() => vol.fstatSync(fd, { bigint: true })).toThrowError(); } }); + it('Returns stats about regular file for fd opened without O_SYMLINK', () => { + const fd = vol.openSync('/link.js', 0); + const stats = vol.fstatSync(fd); + expect(stats).toBeInstanceOf(Stats); + expect(stats.size).toBe(data.length); + expect(stats.isFile()).toBe(true); + expect(stats.isDirectory()).toBe(false); + }); + it('Returns stats about symlink itself for fd opened with O_SYMLINK', () => { + const fd = vol.openSync('/link.js', O_SYMLINK); + const stats = vol.fstatSync(fd); + expect(stats.isSymbolicLink()).toBe(true); + expect(stats.isFile()).toBe(false); + expect(stats.size).toBe(0); + }); }); describe('.fstat(fd, callback)', () => { xit('...', () => {}); diff --git a/src/constants.ts b/src/constants.ts index 51048e822..c5ed8571d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ export const constants = { O_NOATIME: 262144, O_NOFOLLOW: 131072, O_SYNC: 1052672, + O_SYMLINK: 2097152, O_DIRECT: 16384, O_NONBLOCK: 2048, S_IRWXU: 448, diff --git a/src/volume.ts b/src/volume.ts index db46b0417..e77ef17a4 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -68,6 +68,7 @@ const { O_TRUNC, O_APPEND, O_DIRECTORY, + O_SYMLINK, F_OK, COPYFILE_EXCL, COPYFILE_FICLONE_FORCE, @@ -95,6 +96,7 @@ const kMinPoolSpace = 128; // ---------------------------------------- Error messages +const EPERM = 'EPERM'; const ENOENT = 'ENOENT'; const EBADF = 'EBADF'; const EINVAL = 'EINVAL'; @@ -703,7 +705,7 @@ export class Volume implements FsCallbackApi { const modeNum = modeToNumber(mode); const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); - return this.openBase(fileName, flagsNum, modeNum); + return this.openBase(fileName, flagsNum, modeNum, !(flagsNum & O_SYMLINK)); } open(path: PathLike, flags: TFlags, /* ... */ callback: TCallback); @@ -722,7 +724,7 @@ export class Volume implements FsCallbackApi { const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); - this.wrapAsync(this.openBase, [fileName, flagsNum, modeNum], callback); + this.wrapAsync(this.openBase, [fileName, flagsNum, modeNum, !(flagsNum & O_SYMLINK)], callback); } private closeFile(file: File) { @@ -762,6 +764,9 @@ export class Volume implements FsCallbackApi { position: number, ): number { const file = this.getFileByFdOrThrow(fd); + if (file.node.isSymlink()) { + throw createError(EPERM, 'read', file.link.getPath()); + } return file.read(buffer, Number(offset), Number(length), position); } @@ -851,6 +856,9 @@ export class Volume implements FsCallbackApi { private writeBase(fd: number, buf: Buffer, offset?: number, length?: number, position?: number | null): number { const file = this.getFileByFdOrThrow(fd, 'write'); + if (file.node.isSymlink()) { + throw createError(EBADF, 'write', file.link.getPath()); + } return file.write(buf, offset, length, position); }