Skip to content

chmod Calculator — JavaScript source

Compute Unix file permissions between octal (e.g. 755), symbolic (rwxr-xr-x), and decimal - including setuid, setgid, and sticky bits. Toggle permissions interactively, fully client-side, with a shareable link.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * chmod-calculator - POSIX permission mode converter (octal <-> symbolic).
 *
 * Language:   JavaScript (ES modules, runs unmodified in Node 18+ and modern
 *             browsers that support <script type="module">)
 * Source:     CosmoDev polyglot showcase port of the Chmod Calculator tool,
 *             ported from src/lib/chmod.ts (the canonical TypeScript lib) and
 *             held in lock-step with cli/chmod-calculator/chmod-calculator.go.
 * License:    display source - part of CosmoDev's polyglot tool pages.
 *
 * Design goals:
 *   - Pure + deterministic; never throws.
 *   - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
 *   - Self-contained: stdlib only (no npm dependencies).
 *
 * Converts between 3-4 digit octal ("755" / "4755"), 9-char symbolic
 * ("rwxr-xr-x"), and the raw decimal mode, including the setuid / setgid /
 * sticky special bits (the s/S and t/T markers in the exec slot).
 */

import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';

/**
 * Full chmod breakdown - the JS mirror of the TS `ChmodResult` / Go `Result`.
 *
 * @typedef {Object} ChmodResult
 * @property {string}  octal    4-digit zero-padded octal, e.g. "0755".
 * @property {string}  symbolic 9-char `rwxrwxrwx` with special markers, e.g. "rwsr-xr-x".
 * @property {number}  decimal  Raw integer mode (0-4095).
 * @property {boolean} setuid
 * @property {boolean} setgid
 * @property {boolean} sticky
 */

/**
 * Permission class for a triplet - governs which special-bit marker (s/S for
 * owner+group, t/T for other) is legal in its exec slot.
 * @typedef {('owner' | 'group' | 'other')} Pos
 */

/**
 * Parse a 3-char rwx triplet at `pos`. The exec slot may carry a special-bit
 * marker: s/S (setuid in owner, setgid in group) or t/T (sticky in other).
 * Returns `{ digit, special }`, or `null` when invalid.
 *
 * @param {string} tri
 * @param {Pos}    pos
 * @returns {{ digit: number, special: number } | null}
 */
export function parseTriplet(tri, pos) {
  if (tri.length !== 3) return null;
  let digit = 0;

  const r = tri[0];
  if (r === 'r') digit |= 4;
  else if (r !== '-') return null;

  const w = tri[1];
  if (w === 'w') digit |= 2;
  else if (w !== '-') return null;

  let special = 0;
  const c = tri[2];
  if (c === 'x') {
    digit |= 1;
  } else if (c === '-') {
    // no permission
  } else if ((c === 's' || c === 'S') && (pos === 'owner' || pos === 'group')) {
    if (c === 's') digit |= 1;
    special = pos === 'owner' ? 4 : 2;
  } else if ((c === 't' || c === 'T') && pos === 'other') {
    if (c === 't') digit |= 1;
    special = 1;
  } else {
    return null;
  }

  return { digit, special };
}

/**
 * Render a 0-7 digit + optional special bit as a 3-char triplet. `marker` is
 * 's' (owner/group) or 't' (other); upper-cased when the exec bit is absent -
 * yielding 'S' / 'T'.
 *
 * @param {number}  digit
 * @param {boolean} hasSpecial
 * @param {string}  marker
 * @returns {string}
 */
export function formatTriplet(digit, hasSpecial, marker) {
  let out = (digit & 4 ? 'r' : '-') + (digit & 2 ? 'w' : '-');
  const exec = (digit & 1) !== 0;
  if (hasSpecial && exec) out += marker;
  else if (hasSpecial) out += marker.toUpperCase();
  else if (exec) out += 'x';
  else out += '-';
  return out;
}

/**
 * Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or null.
 * @param {string} sym
 * @returns {number | null}
 */
export function symbolicToMode(sym) {
  const s = sym.trim();
  if (s.length !== 9) return null;
  const o = parseTriplet(s.slice(0, 3), 'owner');
  const g = parseTriplet(s.slice(3, 6), 'group');
  const ot = parseTriplet(s.slice(6, 9), 'other');
  if (!o || !g || !ot) return null;
  const special = o.special | g.special | ot.special;
  return special * 0o1000 + (o.digit << 6) + (g.digit << 3) + ot.digit;
}

/**
 * Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or null.
 * @param {string} octal
 * @returns {number | null}
 */
export function octalToMode(octal) {
  const s = octal.trim();
  if (!/^[0-7]{3,4}$/.test(s)) return null;
  return parseInt(s, 8);
}

/**
 * Render a raw mode as 9-char symbolic notation.
 * @param {number} mode
 * @returns {string}
 */
export function modeToSymbolic(mode) {
  const special = (mode >> 9) & 7;
  return (
    formatTriplet((mode >> 6) & 7, (special & 4) !== 0, 's') +
    formatTriplet((mode >> 3) & 7, (special & 2) !== 0, 's') +
    formatTriplet(mode & 7, (special & 1) !== 0, 't')
  );
}

/**
 * Render a raw mode as a 4-digit zero-padded octal string.
 * @param {number} mode
 * @returns {string}
 */
export function modeToOctal(mode) {
  return (mode & 0o7777).toString(8).padStart(4, '0');
}

/**
 * @param {number} mode
 * @returns {ChmodResult}
 */
function buildResult(mode) {
  const special = (mode >> 9) & 7;
  return {
    octal: modeToOctal(mode),
    symbolic: modeToSymbolic(mode),
    decimal: mode & 0o7777,
    setuid: (special & 4) !== 0,
    setgid: (special & 2) !== 0,
    sticky: (special & 1) !== 0,
  };
}

/**
 * Build a full result from symbolic notation, or null if invalid.
 * @param {string} sym
 * @returns {ChmodResult | null}
 */
export function fromSymbolic(sym) {
  const mode = symbolicToMode(sym);
  return mode === null ? null : buildResult(mode);
}

/**
 * Build a full result from an octal string, or null if invalid.
 * @param {string} octal
 * @returns {ChmodResult | null}
 */
export function fromOctal(octal) {
  const mode = octalToMode(octal);
  return mode === null ? null : buildResult(mode);
}

// --- showcase assertions (the canonical suite lives in src/lib) ------------------
// Runs only when executed directly (`node js.js`), never when imported as a module.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const r = fromOctal('755');
  assert.strictEqual(r.octal, '0755');
  assert.strictEqual(r.symbolic, 'rwxr-xr-x');
  assert.strictEqual(r.decimal, 0o755);
  assert.strictEqual(r.setuid || r.setgid || r.sticky, false);

  assert.strictEqual(fromSymbolic('rwxr-xr-x').octal, '0755');

  const su = fromOctal('4755'); // setuid over rwxr-xr-x -> exec slot becomes 's'
  assert.strictEqual(su.symbolic, 'rwsr-xr-x');
  assert.strictEqual(su.decimal, 0o4755);
  assert.strictEqual(su.setuid, true);
  assert.strictEqual(su.sticky, false);

  const st = fromOctal('1644'); // sticky over rw-r--r--, no exec -> marker 'T'
  assert.strictEqual(st.symbolic, 'rw-r--r-T');
  assert.strictEqual(st.decimal, 0o1644);
  assert.strictEqual(st.sticky, true);
  assert.strictEqual(st.setuid, false);

  assert.strictEqual(octalToMode('999'), null); // '9' is not an octal digit
  assert.strictEqual(symbolicToMode('rwx'), null); // wrong length
  assert.strictEqual(fromOctal('0000').symbolic, '---------');

  console.log('All chmod showcase tests passed.');
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →