Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fs/unstable): add chmod and chmodSync #6343

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _tools/node_test_runner/run_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts";
import "../../collections/zip_test.ts";
import "../../fs/unstable_stat_test.ts";
import "../../fs/unstable_lstat_test.ts";
import "../../fs/unstable_chmod_test.ts";

for (const testDef of testDefinitions) {
test(testDef.name, testDef.fn);
Expand Down
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./exists": "./exists.ts",
"./expand-glob": "./expand_glob.ts",
"./move": "./move.ts",
"./unstable-chmod": "./unstable_chmod.ts",
"./unstable-lstat": "./unstable_lstat.ts",
"./unstable-stat": "./unstable_stat.ts",
"./unstable-types": "./unstable_types.ts",
Expand Down
88 changes: 88 additions & 0 deletions fs/unstable_chmod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { getNodeFs, isDeno } from "./_utils.ts";
import { mapError } from "./_map_error.ts";

/**
* Changes the permission of a specific file/directory of specified path.
* Ignores the process's umask.
*
* Requires `allow-write` permission.
*
* The mode is a sequence of 3 octal numbers. The first/left-most number
* specifies the permissions for the owner. The second number specifies the
* permissions for the group. The last/right-most number specifies the
* permissions for others. For example, with a mode of 0o764, the owner (7)
* can read/write/execute, the group (6) can read/write and everyone else (4)
* can read only.
*
* | Number | Description |
* | ------ | ----------- |
* | 7 | read, write, and execute |
* | 6 | read and write |
* | 5 | read and execute |
* | 4 | read only |
* | 3 | write and execute |
* | 2 | write only |
* | 1 | execute only |
* | 0 | no permission |
*
* NOTE: This API currently throws on Windows.
*
* @example Usage
* ```ts no-eval
* import { chmod } from "@std/fs/unstable-chmod";
*
* await chmod("README.md", 0o444);
* ```
*
* @tags allow-write
*
* @param path The path to the file or directory.
* @param mode A sequence of 3 octal numbers representing file permissions.
*/
export async function chmod(path: string | URL, mode: number) {
if (isDeno) {
await Deno.chmod(path, mode);
} else {
try {
await getNodeFs().promises.chmod(path, mode);
} catch (error) {
throw mapError(error);
}
}

Check warning on line 53 in fs/unstable_chmod.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_chmod.ts#L48-L53

Added lines #L48 - L53 were not covered by tests
}

/**
* Synchronously changes the permission of a specific file/directory of
* specified path. Ignores the process's umask.
*
* Requires `allow-write` permission.
*
* For a full description, see {@linkcode chmod}.
*
* NOTE: This API currently throws on Windows.
*
* @example Usage
* ```ts no-eval
* import { chmodSync } from "@std/fs/unstable-chmod";
*
* chmodSync("README.md", 0o666);
* ```
*
* @tags allow-write
*
* @param path The path to the file or directory.
* @param mode A sequence of 3 octal numbers representing permissions. See {@linkcode chmod}.
*/
export function chmodSync(path: string | URL, mode: number) {
if (isDeno) {
Deno.chmodSync(path, mode);
} else {
try {
getNodeFs().chmodSync(path, mode);
} catch (error) {
throw mapError(error);
}
}

Check warning on line 87 in fs/unstable_chmod.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_chmod.ts#L82-L87

Added lines #L82 - L87 were not covered by tests
}
175 changes: 175 additions & 0 deletions fs/unstable_chmod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import {
assertEquals,
assertExists,
assertRejects,
assertThrows,
} from "@std/assert";
import { resolve } from "@std/path";
import { chmod, chmodSync } from "./unstable_chmod.ts";
import { NotFound } from "./unstable_errors.js";

Deno.test({
name: "chmod() sets read only permission bits on regular files",
ignore: Deno.build.os === "windows",
fn: async () => {
const tempDirPath = await Deno.makeTempDir({ prefix: "chmod_" });
const testFile = resolve(tempDirPath, "chmod_file.txt");
using _tempFile = await Deno.create(testFile);

// Check initial testFile permissions are 0o644 (-rw-r--r--).
const fileStatBefore = await Deno.stat(testFile);
assertExists(fileStatBefore.mode, "mode property is null");
assertEquals(fileStatBefore.mode & 0o644, 0o644);

// Set testFile permission bits to read only, 0o444 (-r--r--r--).
await chmod(testFile, 0o444);
const fileStatAfter = await Deno.stat(testFile);
assertExists(fileStatAfter.mode, "mode property is null");
assertEquals(fileStatAfter.mode & 0o444, 0o444);

await Deno.remove(tempDirPath, { recursive: true });
},
});

Deno.test({
name: "chmod() sets read only permission bits on a directory",
ignore: Deno.build.os === "windows",
fn: async () => {
const tempDirPath = await Deno.makeTempDir({ prefix: "chmod_" });
const testDir = resolve(tempDirPath, "testDir");
await Deno.mkdir(testDir);

// Check initial testDir permissions are 0o755 (drwxr-xr-x).
const dirStatBefore = await Deno.stat(testDir);
assertExists(dirStatBefore.mode, "mode property is null");
assertEquals(dirStatBefore.mode & 0o755, 0o755);

// Set testDir permission bits to read only to 0o444 (dr--r--r--).
await chmod(testDir, 0o444);
const dirStatAfter = await Deno.stat(testDir);
assertExists(dirStatAfter.mode, "mode property is null");
assertEquals(dirStatAfter.mode & 0o444, 0o444);

await Deno.remove(tempDirPath, { recursive: true });
},
});

Deno.test({
name: "chmod() sets write only permission bits of regular file via symlink",
ignore: Deno.build.os === "windows",
fn: async () => {
const tempDirPath = await Deno.makeTempDir({ prefix: "chmod_" });
const testFile = resolve(tempDirPath, "chmod_file.txt");
const testSymlink = resolve(tempDirPath, "chmod_file.txt.link");

using _tempFile = await Deno.create(testFile);
await Deno.symlink(testFile, testSymlink);

// Check initial testFile permission bits are 0o644 (-rw-r-xr-x) reading through testSymlink.
const symlinkStatBefore = await Deno.stat(testSymlink);
assertExists(symlinkStatBefore.mode, "mode property via symlink is null");
assertEquals(symlinkStatBefore.mode & 0o644, 0o644);

// Set write only permission bits of testFile through testSymlink to 0o222 (--w--w--w-).
await chmod(testSymlink, 0o222);
const symlinkStatAfter = await Deno.stat(testSymlink);
assertExists(symlinkStatAfter.mode, "mode property via symlink is null");
const fileStatAfter = await Deno.stat(testFile);
assertExists(fileStatAfter.mode, "mode property via file is null");

// Check if both regular file mode and the mode read through symlink are both write only.
assertEquals(symlinkStatAfter.mode, fileStatAfter.mode);

await Deno.remove(tempDirPath, { recursive: true });
},
});

Deno.test("chmod() rejects with NotFound for a non-existent file", async () => {
await assertRejects(async () => {
await chmod("non_existent_file.txt", 0o644);
}, NotFound);
});

Deno.test({
name: "chmodSync() sets read-only permission bits on regular files",
ignore: Deno.build.os === "windows",
fn: () => {
const tempDirPath = Deno.makeTempDirSync({ prefix: "chmodSync_" });
const testFile = resolve(tempDirPath, "chmod_file.txt");
using _tempFile = Deno.createSync(testFile);

// Check initial testFile permissions are 0o644 (-rw-r--r--).
const fileStatBefore = Deno.statSync(testFile);
assertExists(fileStatBefore.mode, "mode property is null");
assertEquals(fileStatBefore.mode & 0o644, 0o644);

// Set testFile permission bits to read only, 0o444 (-r--r--r--).
chmodSync(testFile, 0o444);
const fileStatAfter = Deno.statSync(testFile);
assertExists(fileStatAfter.mode, "mode property is null");
assertEquals(fileStatAfter.mode & 0o444, 0o444);

Deno.removeSync(tempDirPath, { recursive: true });
},
});

Deno.test({
name: "chmodSync() sets read-only permissions bits on directories",
ignore: Deno.build.os === "windows",
fn: () => {
const tempDirPath = Deno.makeTempDirSync({ prefix: "chmodSync_" });
const testDir = resolve(tempDirPath, "testDir");
Deno.mkdirSync(testDir);

// Check initial testDir permissions are 0o755 (drwxr-xr-x).
const dirStatBefore = Deno.statSync(testDir);
assertExists(dirStatBefore.mode, "mode property is null");
assertEquals(dirStatBefore.mode & 0o755, 0o755);

// Set testDir permission bits to read only to 0o444 (dr--r--r--).
chmodSync(testDir, 0o444);
const dirStatAfter = Deno.statSync(testDir);
assertExists(dirStatAfter.mode, "mode property is null");
assertEquals(dirStatAfter.mode & 0o444, 0o444);

Deno.removeSync(tempDirPath, { recursive: true });
},
});

Deno.test({
name: "chmodSync() sets write only permission on a regular file via symlink",
ignore: Deno.build.os === "windows",
fn: () => {
const tempDirPath = Deno.makeTempDirSync({ prefix: "chmodSync_" });
const testFile = resolve(tempDirPath, "chmod_file.txt");
const testSymlink = resolve(tempDirPath, "chmod_file.txt.link");

using _tempFile = Deno.createSync(testFile);
Deno.symlinkSync(testFile, testSymlink);

// Check initial testFile permission bits are 0o644 (-rw-r-xr-x) reading through testSymlink.
const symlinkStatBefore = Deno.statSync(testSymlink);
assertExists(symlinkStatBefore.mode, "mode property via symlink is null");
assertEquals(symlinkStatBefore.mode & 0o644, 0o644);

// Set write only permission bits of testFile through testSymlink to 0o222 (--w--w--w-).
chmodSync(testSymlink, 0o222);
const symlinkStatAfter = Deno.statSync(testSymlink);
assertExists(symlinkStatAfter.mode, "mode property via symlink is null");
const fileStatAfter = Deno.statSync(testFile);
assertExists(fileStatAfter.mode, "mode property via file is null");

// Check if both regular file mode and the mode read through symlink are both write only.
assertEquals(symlinkStatAfter.mode, fileStatAfter.mode);

Deno.removeSync(tempDirPath, { recursive: true });
},
});

Deno.test("chmodSync() throws with NotFound for a non-existent file", () => {
assertThrows(() => {
chmodSync("non_existent_file.txt", 0o644);
}, NotFound);
});
Loading