Repository: denoland/deno
Pull request: fix(ext/node): validate path parameters on `link` and `linkSync`

Changes in this PR:
- Validate the path parameters of `link` and `linkSync`. Implementation is based on [Node.js v23.9](https://github.com/nodejs/node/blob/9d2368f64329bf194c4e82b349e76fdad879d32a/lib/fs.js#L1898-L1933).
- Fix the `prefer-primordials` lint rule.

This changes allows [parallel/test-fs-link.js](https://github.com/denoland/node_test/blob/8846b5392f4ebd3d4e995b9bed61c23c0192d806/test/parallel/test-fs-link.js) to pass.

Towards #29972, #24236

Changed files:
filename: ext/node/polyfills/_fs/_fs_link.ts
patch: @@ -1,50 +1,46 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
-// TODO(petamoriken): enable prefer-primordials for node polyfills
-// deno-lint-ignore-file prefer-primordials
-
+import type { Buffer } from "node:buffer";
 import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts";
-import { pathFromURL } from "ext:deno_web/00_infra.js";
 import { promisify } from "ext:deno_node/internal/util.mjs";
+import { primordials } from "ext:core/mod.js";
+import { getValidatedPathToString } from "ext:deno_node/internal/fs/utils.mjs";
+import * as pathModule from "node:path";
+
+const { PromisePrototypeThen } = primordials;
 
-/**
- * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these
- * are implemented. See https://github.com/denoland/deno/issues/3403
- */
 export function link(
-  existingPath: string | URL,
-  newPath: string | URL,
+  existingPath: string | Buffer | URL,
+  newPath: string | Buffer | URL,
   callback: CallbackWithError,
 ) {
-  existingPath = existingPath instanceof URL
-    ? pathFromURL(existingPath)
-    : existingPath;
-  newPath = newPath instanceof URL ? pathFromURL(newPath) : newPath;
+  existingPath = getValidatedPathToString(existingPath);
+  newPath = getValidatedPathToString(newPath);
 
-  Deno.link(existingPath, newPath).then(() => callback(null), callback);
+  PromisePrototypeThen(
+    Deno.link(
+      pathModule.toNamespacedPath(existingPath),
+      pathModule.toNamespacedPath(newPath),
+    ),
+    () => callback(null),
+    callback,
+  );
 }
 
-/**
- * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these
- * are implemented. See https://github.com/denoland/deno/issues/3403
- */
 export const linkPromise = promisify(link) as (
-  existingPath: string | URL,
-  newPath: string | URL,
+  existingPath: string | Buffer | URL,
+  newPath: string | Buffer | URL,
 ) => Promise<void>;
 
-/**
- * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these
- * are implemented. See https://github.com/denoland/deno/issues/3403
- */
 export function linkSync(
-  existingPath: string | URL,
-  newPath: string | URL,
+  existingPath: string | Buffer | URL,
+  newPath: string | Buffer | URL,
 ) {
-  existingPath = existingPath instanceof URL
-    ? pathFromURL(existingPath)
-    : existingPath;
-  newPath = newPath instanceof URL ? pathFromURL(newPath) : newPath;
+  existingPath = getValidatedPathToString(existingPath);
+  newPath = getValidatedPathToString(newPath);
 
-  Deno.linkSync(existingPath, newPath);
+  Deno.linkSync(
+    pathModule.toNamespacedPath(existingPath),
+    pathModule.toNamespacedPath(newPath),
+  );
 }

filename: tests/node_compat/config.toml
patch: @@ -427,6 +427,7 @@
 "parallel/test-fs-exists.js" = {}
 "parallel/test-fs-existssync-false.js" = {}
 "parallel/test-fs-fchown.js" = {}
+"parallel/test-fs-link.js" = {}
 "parallel/test-fs-long-path.js" = {}
 "parallel/test-fs-promises-exists.js" = {}
 "parallel/test-fs-promises-readfile-empty.js" = {}

filename: tests/unit_node/_fs/_fs_link_test.ts
patch: @@ -3,6 +3,7 @@ import * as path from "@std/path";
 import { assert, assertEquals, fail } from "@std/assert";
 import { assertCallbackErrorUncaught } from "../_test_utils.ts";
 import { link, linkSync } from "node:fs";
+import { Buffer } from "node:buffer";
 
 Deno.test({
   name: "ASYNC: hard linking files works as expected",
@@ -75,3 +76,45 @@ Deno.test("[std/node/fs] link callback isn't called twice if error is thrown", a
     },
   });
 });
+
+Deno.test("[std/node/fs] link accepts Buffer", async () => {
+  const tempDir = await Deno.makeTempDir();
+  const tempFile = path.join(tempDir, "file.txt");
+  const linkedFile = path.join(tempDir, "file.link");
+  const tempFileBuffer = Buffer.from(tempFile, "utf8");
+  const linkedFileBuffer = Buffer.from(linkedFile, "utf8");
+  await Deno.writeTextFile(tempFile, "hello world");
+
+  await new Promise<void>((resolve, reject) => {
+    link(tempFileBuffer, linkedFileBuffer, (err) => {
+      if (err) reject(err);
+      else resolve();
+    });
+  })
+    .then(() => {
+      assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));
+    }, () => {
+      fail("Expected to succeed");
+    })
+    .finally(() => {
+      Deno.removeSync(tempFile);
+      Deno.removeSync(linkedFile);
+      Deno.removeSync(tempDir);
+    });
+});
+
+Deno.test("[std/node/fs] linkSync accepts Buffer", () => {
+  const tempDir = Deno.makeTempDirSync();
+  const tempFile = path.join(tempDir, "file.txt");
+  const linkedFile = path.join(tempDir, "file.link");
+  const tempFileBuffer = Buffer.from(tempFile, "utf8");
+  const linkedFileBuffer = Buffer.from(linkedFile, "utf8");
+  Deno.writeTextFileSync(tempFile, "hello world");
+
+  linkSync(tempFileBuffer, linkedFileBuffer);
+  assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));
+
+  Deno.removeSync(linkedFile);
+  Deno.removeSync(tempFile);
+  Deno.removeSync(tempDir);
+});