Skip to content

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

"""chmod-calculator — POSIX permission mode converter (octal <-> symbolic).

Language: Python (3.9+, 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 raises.
  - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
  - Self-contained: stdlib only (no pip 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).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal, Optional, Tuple

__all__ = [
    "ChmodResult",
    "from_octal",
    "from_symbolic",
    "mode_to_octal",
    "mode_to_symbolic",
    "octal_to_mode",
    "symbolic_to_mode",
]

Pos = Literal["owner", "group", "other"]


@dataclass(frozen=True)
class ChmodResult:
    """Full chmod breakdown — the Python mirror of the TS ``ChmodResult`` /
    Go ``Result`` structs."""

    octal: str
    """4-digit zero-padded octal, e.g. ``"0755"``."""

    symbolic: str
    """9-char ``rwxrwxrwx`` with special markers, e.g. ``"rwsr-xr-x"``."""

    decimal: int
    """Raw integer mode (0-4095)."""

    setuid: bool
    setgid: bool
    sticky: bool


def _parse_triplet(tri: str, pos: Pos) -> Optional[Tuple[int, int]]:
    """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 ``None``
    when invalid.
    """
    if len(tri) != 3:
        return None
    digit = 0
    if tri[0] == "r":
        digit |= 4
    elif tri[0] != "-":
        return None
    if tri[1] == "w":
        digit |= 2
    elif tri[1] != "-":
        return None

    special = 0
    c = tri[2]
    if c == "x":
        digit |= 1
    elif c == "-":
        pass
    elif c in ("s", "S") and pos in ("owner", "group"):
        if c == "s":
            digit |= 1
        special = 4 if pos == "owner" else 2
    elif c in ("t", "T") and pos == "other":
        if c == "t":
            digit |= 1
        special = 1
    else:
        return None
    return (digit, special)


def _format_triplet(digit: int, has_special: bool, marker: str) -> str:
    """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'``.
    """
    out = ("r" if digit & 4 else "-") + ("w" if digit & 2 else "-")
    exec_bit = digit & 1 != 0
    if has_special and exec_bit:
        out += marker
    elif has_special:
        out += marker.upper()
    elif exec_bit:
        out += "x"
    else:
        out += "-"
    return out


def symbolic_to_mode(sym: str) -> Optional[int]:
    """Parse symbolic notation (``"rwxr-xr-x"``) into a raw mode integer, or ``None``."""
    s = sym.strip()
    if len(s) != 9:
        return None
    o = _parse_triplet(s[0:3], "owner")
    g = _parse_triplet(s[3:6], "group")
    ot = _parse_triplet(s[6:9], "other")
    if o is None or g is None or ot is None:
        return None
    special = o[1] | g[1] | ot[1]
    return special * 0o1000 + (o[0] << 6) + (g[0] << 3) + ot[0]


def octal_to_mode(octal: str) -> Optional[int]:
    """Parse a 3-4 digit octal string (``"755"`` / ``"4755"``) into a raw mode, or ``None``."""
    s = octal.strip()
    if len(s) not in (3, 4) or any(c < "0" or c > "7" for c in s):
        return None
    return int(s, 8)


def mode_to_symbolic(mode: int) -> str:
    """Render a raw mode as 9-char symbolic notation."""
    special = (mode >> 9) & 7
    return (
        _format_triplet((mode >> 6) & 7, special & 4 != 0, "s")
        + _format_triplet((mode >> 3) & 7, special & 2 != 0, "s")
        + _format_triplet(mode & 7, special & 1 != 0, "t")
    )


def mode_to_octal(mode: int) -> str:
    """Render a raw mode as a 4-digit zero-padded octal string."""
    return f"{mode & 0o7777:04o}"


def _build_result(mode: int) -> ChmodResult:
    special = (mode >> 9) & 7
    return ChmodResult(
        octal=mode_to_octal(mode),
        symbolic=mode_to_symbolic(mode),
        decimal=mode & 0o7777,
        setuid=special & 4 != 0,
        setgid=special & 2 != 0,
        sticky=special & 1 != 0,
    )


def from_symbolic(sym: str) -> Optional[ChmodResult]:
    """Build a full result from symbolic notation, or ``None`` if invalid."""
    mode = symbolic_to_mode(sym)
    return None if mode is None else _build_result(mode)


def from_octal(octal: str) -> Optional[ChmodResult]:
    """Build a full result from an octal string, or ``None`` if invalid."""
    mode = octal_to_mode(octal)
    return None if mode is None else _build_result(mode)


# --- showcase assertions (the canonical suite lives in src/lib) ------------------
if __name__ == "__main__":
    r = from_octal("755")
    assert r is not None
    assert (r.octal, r.symbolic, r.decimal) == ("0755", "rwxr-xr-x", 0o755)
    assert not (r.setuid or r.setgid or r.sticky)

    assert from_symbolic("rwxr-xr-x").octal == "0755"  # type: ignore[union-attr]

    su = from_octal("4755")  # setuid over rwxr-xr-x -> exec slot becomes 's'
    assert su is not None
    assert (su.symbolic, su.decimal, su.setuid) == ("rwsr-xr-x", 0o4755, True)

    st = from_octal("1644")  # sticky over rw-r--r--, no exec -> marker 'T'
    assert st is not None
    assert (st.symbolic, st.decimal, st.sticky, st.setuid) == ("rw-r--r-T", 0o1644, True, False)

    assert octal_to_mode("999") is None  # '9' is not an octal digit
    assert symbolic_to_mode("rwx") is None  # wrong length
    assert from_octal("0000").symbolic == "---------"  # type: ignore[union-attr]

    print("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 →