Skip to content

Commit 63e3873

Browse files
authored
fix: resolve relative symlinks to the current directory (#1079)
Fixes #725. Symlinks are intended to be stored as relative paths to their target file.
1 parent 1a73187 commit 63e3873

File tree

3 files changed

+71
-11
lines changed

3 files changed

+71
-11
lines changed

src/__tests__/volume.test.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,62 @@ describe('volume', () => {
795795
expect(vol.readFileSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'utf8')).toBe('world a');
796796
});
797797
});
798+
describe('Relative paths', () => {
799+
it('Creates symlinks with relative paths correctly', () => {
800+
const vol = Volume.fromJSON({
801+
'/test/target': 'foo',
802+
'/test/folder': null,
803+
});
804+
805+
// Create symlink using relative path
806+
vol.symlinkSync('../target', '/test/folder/link');
807+
808+
// Verify we can read through the symlink
809+
expect(vol.readFileSync('/test/folder/link', 'utf8')).toBe('foo');
810+
811+
// Verify the symlink points to the correct location
812+
const linkPath = vol.readlinkSync('/test/folder/link');
813+
expect(linkPath).toBe('../target');
814+
});
815+
816+
it('Handles nested relative symlinks', () => {
817+
const vol = Volume.fromJSON({
818+
'/a/b/target.txt': 'content',
819+
'/a/c/d': null,
820+
});
821+
822+
// Create symlink in nested directory using relative path
823+
vol.symlinkSync('../../b/target.txt', '/a/c/d/link');
824+
825+
// Should be able to read through the symlink
826+
expect(vol.readFileSync('/a/c/d/link', 'utf8')).toBe('content');
827+
828+
// Create another symlink pointing to the first symlink
829+
vol.symlinkSync('./d/link', '/a/c/link2');
830+
831+
// Should be able to read through both symlinks
832+
expect(vol.readFileSync('/a/c/link2', 'utf8')).toBe('content');
833+
});
834+
835+
it('Maintains relative paths when reading symlinks', () => {
836+
const vol = Volume.fromJSON({
837+
'/x/y/file.txt': 'test content',
838+
'/x/z': null,
839+
});
840+
841+
// Create symlinks with different relative path patterns
842+
vol.symlinkSync('../y/file.txt', '/x/z/link1');
843+
vol.symlinkSync('../../x/y/file.txt', '/x/z/link2');
844+
845+
// Verify that readlink returns the original relative paths
846+
expect(vol.readlinkSync('/x/z/link1')).toBe('../y/file.txt');
847+
expect(vol.readlinkSync('/x/z/link2')).toBe('../../x/y/file.txt');
848+
849+
// Verify that all symlinks resolve correctly
850+
expect(vol.readFileSync('/x/z/link1', 'utf8')).toBe('test content');
851+
expect(vol.readFileSync('/x/z/link2', 'utf8')).toBe('test content');
852+
});
853+
});
798854
});
799855
describe('.symlink(target, path[, type], callback)', () => {
800856
xit('...', () => {});
@@ -806,7 +862,7 @@ describe('volume', () => {
806862
mootools.getNode().setString(data);
807863

808864
const symlink = vol.root.createChild('mootools.link.js');
809-
symlink.getNode().makeSymlink(['mootools.js']);
865+
symlink.getNode().makeSymlink('mootools.js');
810866

811867
it('Symlink works', () => {
812868
const resolved = vol.resolveSymlinks(symlink);
@@ -828,7 +884,7 @@ describe('volume', () => {
828884
mootools.getNode().setString(data);
829885

830886
const symlink = vol.root.createChild('mootools.link.js');
831-
symlink.getNode().makeSymlink(['mootools.js']);
887+
symlink.getNode().makeSymlink('mootools.js');
832888

833889
it('Basic one-jump symlink resolves', done => {
834890
vol.realpath('/mootools.link.js', (err, path) => {

src/node.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export class Node extends EventEmitter {
3636
// Number of hard links pointing at this Node.
3737
private _nlink = 1;
3838

39-
// Steps to another node, if this node is a symlink.
40-
symlink: string[];
39+
// Path to another node, if this is a symlink.
40+
symlink: string;
4141

4242
constructor(ino: number, perm: number = 0o666) {
4343
super();
@@ -163,9 +163,9 @@ export class Node extends EventEmitter {
163163
return (this.mode & S_IFMT) === S_IFLNK;
164164
}
165165

166-
makeSymlink(steps: string[]) {
167-
this.symlink = steps;
168-
this.setIsSymlink();
166+
makeSymlink(symlink: string) {
167+
this.mode = S_IFLNK;
168+
this.symlink = symlink;
169169
}
170170

171171
write(buf: Buffer, off: number = 0, len: number = buf.length, pos: number = 0): number {

src/volume.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,11 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
472472
node = curr?.getNode();
473473
// Resolve symlink
474474
if (resolveSymlinks && node.isSymlink()) {
475-
steps = node.symlink.concat(steps.slice(i + 1));
475+
const resolvedPath = pathModule.isAbsolute(node.symlink)
476+
? node.symlink
477+
: join(pathModule.dirname(curr.getPath()), node.symlink); // Relative to symlink's parent
478+
479+
steps = filenameToSteps(resolvedPath).concat(steps.slice(i + 1));
476480
curr = this.root;
477481
i = 0;
478482
continue;
@@ -1294,7 +1298,8 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
12941298

12951299
// Create symlink.
12961300
const symlink: Link = dirLink.createChild(name);
1297-
symlink.getNode().makeSymlink(filenameToSteps(targetFilename));
1301+
symlink.getNode().makeSymlink(targetFilename);
1302+
12981303
return symlink;
12991304
}
13001305

@@ -1637,8 +1642,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
16371642

16381643
if (!node.isSymlink()) throw createError(EINVAL, 'readlink', filename);
16391644

1640-
const str = sep + node.symlink.join(sep);
1641-
return strToEncoding(str, encoding);
1645+
return strToEncoding(node.symlink, encoding);
16421646
}
16431647

16441648
readlinkSync(path: PathLike, options?: opts.IOptions): TDataOut {

0 commit comments

Comments
 (0)