From b9854b2e99d80d5edacc01242912dce3a62475e7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 13 Dec 2024 19:50:18 -0500 Subject: [PATCH] fix: resolve relative symlinks to the current directory Fixes #725. Symlinks are intended to be stored as relative paths to their target file. --- src/__tests__/volume.test.ts | 60 ++++++++++++++++++++++++++++++++++-- src/node.ts | 10 +++--- src/volume.ts | 12 +++++--- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 7c7798d12..278e23e32 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -795,6 +795,62 @@ describe('volume', () => { expect(vol.readFileSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'utf8')).toBe('world a'); }); }); + describe('Relative paths', () => { + it('Creates symlinks with relative paths correctly', () => { + const vol = Volume.fromJSON({ + '/test/target': 'foo', + '/test/folder': null, + }); + + // Create symlink using relative path + vol.symlinkSync('../target', '/test/folder/link'); + + // Verify we can read through the symlink + expect(vol.readFileSync('/test/folder/link', 'utf8')).toBe('foo'); + + // Verify the symlink points to the correct location + const linkPath = vol.readlinkSync('/test/folder/link'); + expect(linkPath).toBe('../target'); + }); + + it('Handles nested relative symlinks', () => { + const vol = Volume.fromJSON({ + '/a/b/target.txt': 'content', + '/a/c/d': null, + }); + + // Create symlink in nested directory using relative path + vol.symlinkSync('../../b/target.txt', '/a/c/d/link'); + + // Should be able to read through the symlink + expect(vol.readFileSync('/a/c/d/link', 'utf8')).toBe('content'); + + // Create another symlink pointing to the first symlink + vol.symlinkSync('./d/link', '/a/c/link2'); + + // Should be able to read through both symlinks + expect(vol.readFileSync('/a/c/link2', 'utf8')).toBe('content'); + }); + + it('Maintains relative paths when reading symlinks', () => { + const vol = Volume.fromJSON({ + '/x/y/file.txt': 'test content', + '/x/z': null, + }); + + // Create symlinks with different relative path patterns + vol.symlinkSync('../y/file.txt', '/x/z/link1'); + vol.symlinkSync('../../x/y/file.txt', '/x/z/link2'); + + // Verify that readlink returns the original relative paths + expect(vol.readlinkSync('/x/z/link1')).toBe('../y/file.txt'); + expect(vol.readlinkSync('/x/z/link2')).toBe('../../x/y/file.txt'); + + // Verify that all symlinks resolve correctly + expect(vol.readFileSync('/x/z/link1', 'utf8')).toBe('test content'); + expect(vol.readFileSync('/x/z/link2', 'utf8')).toBe('test content'); + }); + }); }); describe('.symlink(target, path[, type], callback)', () => { xit('...', () => {}); @@ -806,7 +862,7 @@ describe('volume', () => { mootools.getNode().setString(data); const symlink = vol.root.createChild('mootools.link.js'); - symlink.getNode().makeSymlink(['mootools.js']); + symlink.getNode().makeSymlink('mootools.js'); it('Symlink works', () => { const resolved = vol.resolveSymlinks(symlink); @@ -828,7 +884,7 @@ describe('volume', () => { mootools.getNode().setString(data); const symlink = vol.root.createChild('mootools.link.js'); - symlink.getNode().makeSymlink(['mootools.js']); + symlink.getNode().makeSymlink('mootools.js'); it('Basic one-jump symlink resolves', done => { vol.realpath('/mootools.link.js', (err, path) => { diff --git a/src/node.ts b/src/node.ts index 8128e364c..1921fb008 100644 --- a/src/node.ts +++ b/src/node.ts @@ -36,8 +36,8 @@ export class Node extends EventEmitter { // Number of hard links pointing at this Node. private _nlink = 1; - // Steps to another node, if this node is a symlink. - symlink: string[]; + // Path to another node, if this is a symlink. + symlink: string; constructor(ino: number, perm: number = 0o666) { super(); @@ -163,9 +163,9 @@ export class Node extends EventEmitter { return (this.mode & S_IFMT) === S_IFLNK; } - makeSymlink(steps: string[]) { - this.symlink = steps; - this.setIsSymlink(); + makeSymlink(symlink: string) { + this.mode = S_IFLNK; + this.symlink = symlink; } write(buf: Buffer, off: number = 0, len: number = buf.length, pos: number = 0): number { diff --git a/src/volume.ts b/src/volume.ts index f685f6109..0d941e9c2 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -472,7 +472,11 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { node = curr?.getNode(); // Resolve symlink if (resolveSymlinks && node.isSymlink()) { - steps = node.symlink.concat(steps.slice(i + 1)); + const resolvedPath = pathModule.isAbsolute(node.symlink) + ? node.symlink + : join(pathModule.dirname(curr.getPath()), node.symlink); // Relative to symlink's parent + + steps = filenameToSteps(resolvedPath).concat(steps.slice(i + 1)); curr = this.root; i = 0; continue; @@ -1294,7 +1298,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { // Create symlink. const symlink: Link = dirLink.createChild(name); - symlink.getNode().makeSymlink(filenameToSteps(targetFilename)); + symlink.getNode().makeSymlink(targetFilename); + return symlink; } @@ -1637,8 +1642,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { if (!node.isSymlink()) throw createError(EINVAL, 'readlink', filename); - const str = sep + node.symlink.join(sep); - return strToEncoding(str, encoding); + return strToEncoding(node.symlink, encoding); } readlinkSync(path: PathLike, options?: opts.IOptions): TDataOut {