Skip to content

Commit

Permalink
feat(fs/unstable): add chmod and chmodSync (#6343)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder authored Jan 17, 2025
1 parent b1b557e commit 5d41054
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
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 @@ -52,6 +52,7 @@ import "../../collections/zip_test.ts";
import "../../fs/unstable_read_dir_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-read-dir": "./unstable_read_dir.ts",
"./unstable-stat": "./unstable_stat.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 ignore
* 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);
}
}
}

/**
* 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 ignore
* 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);
}
}
}
190 changes: 190 additions & 0 deletions fs/unstable_chmod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import {
assertEquals,
assertExists,
assertRejects,
assertThrows,
} from "@std/assert";
import { chmod, chmodSync } from "./unstable_chmod.ts";
import { NotFound } from "./unstable_errors.js";
import { platform, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { mkdir, mkdtemp, open, rm, stat, symlink } from "node:fs/promises";
import {
closeSync,
mkdirSync,
mkdtempSync,
openSync,
rmSync,
statSync,
symlinkSync,
} from "node:fs";

Deno.test({
name: "chmod() sets read only permission bits on regular files",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "chmod_"));
const testFile = join(tempDirPath, "chmod_file.txt");
const tempFh = await open(testFile, "w");

// Check initial testFile permissions are 0o644 (-rw-r--r--).
const fileStatBefore = await 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 stat(testFile);
assertExists(fileStatAfter.mode, "mode property is null");
assertEquals(fileStatAfter.mode & 0o444, 0o444);

await tempFh.close();
await rm(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name: "chmod() sets read only permission bits on a directory",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "chmod_"));
const testDir = resolve(tempDirPath, "testDir");
await mkdir(testDir);

// Check initial testDir permissions are 0o755 (drwxr-xr-x).
const dirStatBefore = await 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 stat(testDir);
assertExists(dirStatAfter.mode, "mode property is null");
assertEquals(dirStatAfter.mode & 0o444, 0o444);

await rm(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name: "chmod() sets write only permission bits of regular file via symlink",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "chmod_"));
const testFile = resolve(tempDirPath, "chmod_file.txt");
const testSymlink = resolve(tempDirPath, "chmod_file.txt.link");

const tempFh = await open(testFile, "w");
await symlink(testFile, testSymlink);

// Check initial testFile permission bits are 0o644 (-rw-r-xr-x) reading through testSymlink.
const symlinkStatBefore = await 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 stat(testSymlink);
assertExists(symlinkStatAfter.mode, "mode property via symlink is null");
const fileStatAfter = await 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 tempFh.close();
await rm(tempDirPath, { recursive: true, force: 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: platform() === "win32",
fn: () => {
const tempDirPath = mkdtempSync(resolve(tmpdir(), "chmodSync_"));
const testFile = resolve(tempDirPath, "chmod_file.txt");
const tempFd = openSync(testFile, "w");

// Check initial testFile permissions are 0o644 (-rw-r--r--).
const fileStatBefore = 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 = statSync(testFile);
assertExists(fileStatAfter.mode, "mode property is null");
assertEquals(fileStatAfter.mode & 0o444, 0o444);

closeSync(tempFd);
rmSync(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name: "chmodSync() sets read-only permissions bits on directories",
ignore: platform() === "win32",
fn: () => {
const tempDirPath = mkdtempSync(resolve(tmpdir(), "chmodSync_"));
const testDir = resolve(tempDirPath, "testDir");
mkdirSync(testDir);

// Check initial testDir permissions are 0o755 (drwxr-xr-x).
const dirStatBefore = 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 = statSync(testDir);
assertExists(dirStatAfter.mode, "mode property is null");
assertEquals(dirStatAfter.mode & 0o444, 0o444);

rmSync(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name: "chmodSync() sets write only permission on a regular file via symlink",
ignore: platform() === "win32",
fn: () => {
const tempDirPath = mkdtempSync(resolve(tmpdir(), "chmodSync_"));
const testFile = resolve(tempDirPath, "chmod_file.txt");
const testSymlink = resolve(tempDirPath, "chmod_file.txt.link");

const tempFd = openSync(testFile, "w");
symlinkSync(testFile, testSymlink);

// Check initial testFile permission bits are 0o644 (-rw-r-xr-x) reading through testSymlink.
const symlinkStatBefore = 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 = statSync(testSymlink);
assertExists(symlinkStatAfter.mode, "mode property via symlink is null");
const fileStatAfter = 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);

closeSync(tempFd);
rmSync(tempDirPath, { recursive: true, force: true });
},
});

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

0 comments on commit 5d41054

Please sign in to comment.