From 77c513d31fdee9420be7789636eb7211a34ac2bf Mon Sep 17 00:00:00 2001 From: James Bronder <36022278+jbronder@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:29:47 -0800 Subject: [PATCH 1/2] feat(fs/unstable): add import references for `readLink` --- _tools/node_test_runner/run_test.mjs | 1 + fs/deno.json | 1 + 2 files changed, 2 insertions(+) diff --git a/_tools/node_test_runner/run_test.mjs b/_tools/node_test_runner/run_test.mjs index 609b9052df9b..79b10428d9f1 100644 --- a/_tools/node_test_runner/run_test.mjs +++ b/_tools/node_test_runner/run_test.mjs @@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts"; import "../../collections/zip_test.ts"; import "../../fs/unstable_link_test.ts"; import "../../fs/unstable_read_dir_test.ts"; +import "../../fs/unstable_read_link_test.ts"; import "../../fs/unstable_real_path_test.ts"; import "../../fs/unstable_stat_test.ts"; import "../../fs/unstable_symlink_test.ts"; diff --git a/fs/deno.json b/fs/deno.json index 78b4db0e0b0a..608d45085d50 100644 --- a/fs/deno.json +++ b/fs/deno.json @@ -17,6 +17,7 @@ "./unstable-link": "./unstable_link.ts", "./unstable-lstat": "./unstable_lstat.ts", "./unstable-read-dir": "./unstable_read_dir.ts", + "./unstable-read-link": "./unstable_read_link.ts", "./unstable-real-path": "./unstable_real_path.ts", "./unstable-stat": "./unstable_stat.ts", "./unstable-symlink": "./unstable_symlink.ts", From 4e2630398fc8edfb8d943fa3b21882380047e86c Mon Sep 17 00:00:00 2001 From: James Bronder <36022278+jbronder@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:31:02 -0800 Subject: [PATCH 2/2] feat(fs/unstable): add readLink functions and tests --- fs/unstable_read_link.ts | 69 ++++++++++++++++++++++++++++ fs/unstable_read_link_test.ts | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 fs/unstable_read_link.ts create mode 100644 fs/unstable_read_link_test.ts diff --git a/fs/unstable_read_link.ts b/fs/unstable_read_link.ts new file mode 100644 index 000000000000..3b9f75f36371 --- /dev/null +++ b/fs/unstable_read_link.ts @@ -0,0 +1,69 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +import { getNodeFs, isDeno } from "./_utils.ts"; +import { mapError } from "./_map_error.ts"; + +/** + * Resolves to the path destination of the named symbolic link. + * + * Throws Error if called with a hard link. + * + * Requires `allow-read` permission. + * + * @example Usage + * ```ts ignore + * import { readLink } from "@std/fs/unstable-read-link"; + * import { symlink } from "@std/fs/unstable-symlink"; + * await symlink("./test.txt", "./test_link.txt"); + * const target = await readLink("./test_link.txt"); // full path of ./test.txt + * ``` + * + * @tags allow-read + * + * @param path The path of the symbolic link. + * @returns A promise that resolves to the file path pointed by the symbolic + * link. + */ +export async function readLink(path: string | URL): Promise { + if (isDeno) { + return Deno.readLink(path); + } else { + try { + return await getNodeFs().promises.readlink(path); + } catch (error) { + throw mapError(error); + } + } +} + +/** + * Synchronously returns the path destination of the named symbolic link. + * + * Throws Error if called with a hard link. + * + * Requires `allow-read` permission. + * + * @example Usage + * ```ts ignore + * import { readLinkSync } from "@std/fs/unstable-read-link"; + * import { symlinkSync } from "@std/fs/unstable-symlink"; + * symlinkSync("./test.txt", "./test_link.txt"); + * const target = readLinkSync("./test_link.txt"); // full path of ./test.txt + * ``` + * + * @tags allow-read + * + * @param path The path of the symbolic link. + * @returns The file path pointed by the symbolic link. + */ +export function readLinkSync(path: string | URL): string { + if (isDeno) { + return Deno.readLinkSync(path); + } else { + try { + return getNodeFs().readlinkSync(path); + } catch (error) { + throw mapError(error); + } + } +} diff --git a/fs/unstable_read_link_test.ts b/fs/unstable_read_link_test.ts new file mode 100644 index 000000000000..0ef85e6a8753 --- /dev/null +++ b/fs/unstable_read_link_test.ts @@ -0,0 +1,85 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +import { assertEquals, assertRejects, assertThrows } from "@std/assert"; +import { readLink, readLinkSync } from "./unstable_read_link.ts"; +import { NotFound } from "./unstable_errors.js"; +import { + linkSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { link, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +Deno.test("readLink() can read through symlink", async () => { + const tempDirPath = await mkdtemp(resolve(tmpdir(), "readLink_")); + const testFile = join(tempDirPath, "testFile.txt"); + const symlinkFile = join(tempDirPath, "testFile.txt.link"); + + await writeFile(testFile, "Hello, Standard Library"); + await symlink(testFile, symlinkFile); + + const realFile = await readLink(symlinkFile); + assertEquals(testFile, realFile); + + await rm(tempDirPath, { recursive: true, force: true }); +}); + +Deno.test("readLink() rejects with Error when reading from a hard link", async () => { + const tempDirPath = await mkdtemp(resolve(tmpdir(), "readLink_")); + const testFile = join(tempDirPath, "testFile.txt"); + const linkFile = join(tempDirPath, "testFile.txt.hlink"); + + await writeFile(testFile, "Hello, Standard Library"); + await link(testFile, linkFile); + + await assertRejects(async () => { + await readLink(linkFile); + }, Error); + + await rm(tempDirPath, { recursive: true, force: true }); +}); + +Deno.test("readLink() rejects with NotFound when reading through a non-existent file", async () => { + await assertRejects(async () => { + await readLink("non-existent-file.txt.link"); + }, NotFound); +}); + +Deno.test("readLinkSync() can read through symlink", () => { + const tempDirPath = mkdtempSync(resolve(tmpdir(), "readLink_")); + const testFile = join(tempDirPath, "testFile.txt"); + const symlinkFile = join(tempDirPath, "testFile.txt.link"); + + writeFileSync(testFile, "Hello, Standard Library"); + symlinkSync(testFile, symlinkFile); + + const realFile = readLinkSync(symlinkFile); + assertEquals(testFile, realFile); + + rmSync(tempDirPath, { recursive: true, force: true }); +}); + +Deno.test("readLinkSync() throws Error when reading from a hard link", () => { + const tempDirPath = mkdtempSync(resolve(tmpdir(), "readLinkSync_")); + const testFile = join(tempDirPath, "testFile.txt"); + const linkFile = join(tempDirPath, "testFile.txt.hlink"); + + writeFileSync(testFile, "Hello, Standard Library!"); + linkSync(testFile, linkFile); + + assertThrows(() => { + readLinkSync(linkFile); + }, Error); + + rmSync(tempDirPath, { recursive: true, force: true }); +}); + +Deno.test("readLinkSync() throws NotFound when reading through a non-existent file", () => { + assertThrows(() => { + readLinkSync("non-existent-file.txt.hlink"); + }, NotFound); +});