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 →