Skip to content

chmod Calculator — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
/**
 * chmod-calculator — POSIX permission mode converter (octal <-> symbolic).
 *
 * Language: PHP (8.1+, standard library only)
 * 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 Composer packages).
 *
 * 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).
 */

declare(strict_types=1);

/**
 * 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.
 */
const CHMOD_POS_OWNER = 'owner';
const CHMOD_POS_GROUP = 'group';
const CHMOD_POS_OTHER = 'other';

/**
 * 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]` (both int) on
 * success, or `null` when invalid.
 *
 * @return array{0: int, 1: int}|null
 */
function chmod_parse_triplet(string $tri, string $pos): ?array
{
    if (strlen($tri) !== 3) {
        return null;
    }
    $digit = 0;

    $r = $tri[0];
    if ($r === 'r') {
        $digit |= 4;
    } elseif ($r !== '-') {
        return null;
    }

    $w = $tri[1];
    if ($w === 'w') {
        $digit |= 2;
    } elseif ($w !== '-') {
        return null;
    }

    $special = 0;
    $c = $tri[2];
    if ($c === 'x') {
        $digit |= 1;
    } elseif ($c === '-') {
        // no permission
    } elseif (($c === 's' || $c === 'S') && ($pos === CHMOD_POS_OWNER || $pos === CHMOD_POS_GROUP)) {
        if ($c === 's') {
            $digit |= 1;
        }
        $special = $pos === CHMOD_POS_OWNER ? 4 : 2;
    } elseif (($c === 't' || $c === 'T') && $pos === CHMOD_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'.
 */
function chmod_format_triplet(int $digit, bool $has_special, string $marker): string
{
    $out = ($digit & 4) ? 'r' : '-';
    $out .= ($digit & 2) ? 'w' : '-';
    $exec = ($digit & 1) !== 0;
    if ($has_special && $exec) {
        $out .= $marker;
    } elseif ($has_special) {
        $out .= strtoupper($marker);
    } elseif ($exec) {
        $out .= 'x';
    } else {
        $out .= '-';
    }
    return $out;
}

/**
 * Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or null.
 */
function chmod_symbolic_to_mode(string $sym): ?int
{
    $s = trim($sym);
    if (strlen($s) !== 9) {
        return null;
    }
    $o = chmod_parse_triplet(substr($s, 0, 3), CHMOD_POS_OWNER);
    $g = chmod_parse_triplet(substr($s, 3, 3), CHMOD_POS_GROUP);
    $ot = chmod_parse_triplet(substr($s, 6, 3), CHMOD_POS_OTHER);
    if ($o === null || $g === null || $ot === null) {
        return null;
    }
    $special = $o[1] | $g[1] | $ot[1];
    return $special * 0o1000 + ($o[0] << 6) + ($g[0] << 3) + $ot[0];
}

/**
 * Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or null.
 */
function chmod_octal_to_mode(string $octal): ?int
{
    $s = trim($octal);
    $len = strlen($s);
    if ($len !== 3 && $len !== 4) {
        return null;
    }
    // intval(, 8) is lenient about trailing chars; validate [0-7] first so we
    // reject "987" exactly the way the TS regex /^[0-7]{3,4}$/ does.
    for ($i = 0; $i < $len; $i++) {
        if ($s[$i] < '0' || $s[$i] > '7') {
            return null;
        }
    }
    return intval($s, 8);
}

/**
 * Render a raw mode as 9-char symbolic notation.
 */
function chmod_mode_to_symbolic(int $mode): string
{
    $special = ($mode >> 9) & 7;
    return chmod_format_triplet(($mode >> 6) & 7, ($special & 4) !== 0, 's')
        . chmod_format_triplet(($mode >> 3) & 7, ($special & 2) !== 0, 's')
        . chmod_format_triplet($mode & 7, ($special & 1) !== 0, 't');
}

/**
 * Render a raw mode as a 4-digit zero-padded octal string.
 */
function chmod_mode_to_octal(int $mode): string
{
    return sprintf('%04o', $mode & 0o7777);
}

/**
 * Build a full result array from a raw mode. The associative array mirrors the
 * TS `ChmodResult` / Go `Result` struct field-for-field.
 *
 * @return array{octal: string, symbolic: string, decimal: int, setuid: bool, setgid: bool, sticky: bool}
 */
function chmod_build_result(int $mode): array
{
    $special = ($mode >> 9) & 7;
    return [
        'octal' => chmod_mode_to_octal($mode),
        'symbolic' => chmod_mode_to_symbolic($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.
 *
 * @return array{octal: string, symbolic: string, decimal: int, setuid: bool, setgid: bool, sticky: bool}|null
 */
function chmod_from_symbolic(string $sym): ?array
{
    $mode = chmod_symbolic_to_mode($sym);
    return $mode === null ? null : chmod_build_result($mode);
}

/**
 * Build a full result from an octal string, or null if invalid.
 *
 * @return array{octal: string, symbolic: string, decimal: int, setuid: bool, setgid: bool, sticky: bool}|null
 */
function chmod_from_octal(string $octal): ?array
{
    $mode = chmod_octal_to_mode($octal);
    return $mode === null ? null : chmod_build_result($mode);
}

// --- showcase assertions (the canonical suite lives in src/lib) ------------------
// Runs only when this file is invoked directly (`php php.php`), never when it is
// included as a library: an empty debug_backtrace means no caller required it.
if (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) === []) {
    $r = chmod_from_octal('755');
    assert($r !== null);
    assert([$r['octal'], $r['symbolic'], $r['decimal']] === ['0755', 'rwxr-xr-x', 0o755]);
    assert($r['setuid'] === false && $r['setgid'] === false && $r['sticky'] === false);

    assert(chmod_from_symbolic('rwxr-xr-x')['octal'] === '0755');

    $su = chmod_from_octal('4755'); // setuid over rwxr-xr-x -> exec slot becomes 's'
    assert($su !== null);
    assert([$su['symbolic'], $su['decimal'], $su['setuid']] === ['rwsr-xr-x', 0o4755, true]);

    $st = chmod_from_octal('1644'); // sticky over rw-r--r--, no exec -> marker 'T'
    assert($st !== null);
    assert([$st['symbolic'], $st['decimal'], $st['sticky'], $st['setuid']] === ['rw-r--r-T', 0o1644, true, false]);

    assert(chmod_octal_to_mode('999') === null); // '9' is not an octal digit
    assert(chmod_symbolic_to_mode('rwx') === null); // wrong length
    assert(chmod_from_octal('0000')['symbolic'] === '---------');

    echo "All chmod showcase tests passed.\n";
}

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 →