Skip to content

Commit

Permalink
feat(fs/unstable): added chmod and chmodSync functions with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder committed Jan 11, 2025
1 parent 1a7cb83 commit ca14f0d
Show file tree
Hide file tree
Showing 2 changed files with 264 additions and 0 deletions.
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
* 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
* 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
}
176 changes: 176 additions & 0 deletions fs/unstable_chmod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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 through the 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 through a 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);
});

0 comments on commit ca14f0d

Please sign in to comment.