fix(fs/ensure_symlink): check symlink is pointing the given target (#4371)

This commit is contained in:
ryota2357 2024-02-28 11:50:33 +09:00 committed by GitHub
parent ef6b95f0c7
commit 1e0764dc23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 0 deletions

View File

@ -19,6 +19,8 @@ function resolveSymlinkTarget(target: string | URL, linkName: string | URL) {
/**
* Ensures that the link exists, and points to a valid file.
* If the directory structure does not exist, it is created.
* If the link already exists, it is not modified but error is thrown if it is not point to the given target.
* Requires the `--allow-read` and `--allow-write` flag.
*
* @param target the source file path
* @param linkName the destination link path
@ -45,12 +47,28 @@ export async function ensureSymlink(
if (!(error instanceof Deno.errors.AlreadyExists)) {
throw error;
}
const linkStatInfo = await Deno.lstat(linkName);
if (!linkStatInfo.isSymlink) {
const type = getFileInfoType(linkStatInfo);
throw new Deno.errors.AlreadyExists(
`A '${type}' already exists at the path: ${linkName}`,
);
}
const linkPath = await Deno.readLink(linkName);
const linkRealPath = resolve(linkPath);
if (linkRealPath !== targetRealPath) {
throw new Deno.errors.AlreadyExists(
`A symlink targeting to an undesired path already exists: ${linkName} -> ${linkRealPath}`,
);
}
}
}
/**
* Ensures that the link exists, and points to a valid file.
* If the directory structure does not exist, it is created.
* If the link already exists, it is not modified but error is thrown if it is not point to the given target.
* Requires the `--allow-read` and `--allow-write` flag.
*
* @param target the source file path
* @param linkName the destination link path
@ -77,5 +95,19 @@ export function ensureSymlinkSync(
if (!(error instanceof Deno.errors.AlreadyExists)) {
throw error;
}
const linkStatInfo = Deno.lstatSync(linkName);
if (!linkStatInfo.isSymlink) {
const type = getFileInfoType(linkStatInfo);
throw new Deno.errors.AlreadyExists(
`A '${type}' already exists at the path: ${linkName}`,
);
}
const linkPath = Deno.readLinkSync(linkName);
const linkRealPath = resolve(linkPath);
if (linkRealPath !== targetRealPath) {
throw new Deno.errors.AlreadyExists(
`A symlink targeting to an undesired path already exists: ${linkName} -> ${linkRealPath}`,
);
}
}
}

View File

@ -80,6 +80,74 @@ Deno.test("ensureSymlinkSync() ensures linkName links to target", function () {
Deno.removeSync(testDir, { recursive: true });
});
Deno.test("ensureSymlink() rejects if the linkName path already exist", async function () {
const testDir = path.join(testdataDir, "link_file_5");
const linkFile = path.join(testDir, "test.txt");
const linkDir = path.join(testDir, "test_dir");
const linkSymlink = path.join(testDir, "test_symlink");
const targetFile = path.join(testDir, "target.txt");
await Deno.mkdir(testDir, { recursive: true });
await Deno.writeTextFile(linkFile, "linkFile");
await Deno.mkdir(linkDir);
await Deno.symlink("non-existent", linkSymlink, { type: "file" });
await Deno.writeTextFile(targetFile, "targetFile");
await assertRejects(
async () => {
await ensureSymlink(targetFile, linkFile);
},
);
await assertRejects(
async () => {
await ensureSymlink(targetFile, linkDir);
},
);
await assertRejects(
async () => {
await ensureSymlink(targetFile, linkSymlink);
},
);
assertEquals(await Deno.readTextFile(linkFile), "linkFile");
assertEquals((await Deno.stat(linkDir)).isDirectory, true);
assertEquals(await Deno.readLink(linkSymlink), "non-existent");
assertEquals(await Deno.readTextFile(targetFile), "targetFile");
await Deno.remove(testDir, { recursive: true });
});
Deno.test("ensureSymlinkSync() throws if the linkName path already exist", function () {
const testDir = path.join(testdataDir, "link_file_6");
const linkFile = path.join(testDir, "test.txt");
const linkDir = path.join(testDir, "test_dir");
const linkSymlink = path.join(testDir, "test_symlink");
const targetFile = path.join(testDir, "target.txt");
Deno.mkdirSync(testDir, { recursive: true });
Deno.writeTextFileSync(linkFile, "linkFile");
Deno.mkdirSync(linkDir);
Deno.symlinkSync("non-existent", linkSymlink, { type: "file" });
Deno.writeTextFileSync(targetFile, "targetFile");
assertThrows(() => {
ensureSymlinkSync(targetFile, linkFile);
});
assertThrows(() => {
ensureSymlinkSync(targetFile, linkDir);
});
assertThrows(() => {
ensureSymlinkSync(targetFile, linkSymlink);
});
assertEquals(Deno.readTextFileSync(linkFile), "linkFile");
assertEquals(Deno.statSync(linkDir).isDirectory, true);
assertEquals(Deno.readLinkSync(linkSymlink), "non-existent");
assertEquals(Deno.readTextFileSync(targetFile), "targetFile");
Deno.removeSync(testDir, { recursive: true });
});
Deno.test("ensureSymlink() ensures dir linkName links to dir target", async function () {
const testDir = path.join(testdataDir, "link_file_origin_3");
const linkDir = path.join(testdataDir, "link_file_link_3");