Skip to content

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

// chmod-calculator — POSIX permission mode converter (octal <-> symbolic).
//
// Language: C++ (C++17, 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; failable public API returns std::optional (never throws).
//   - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
//   - Self-contained: std only (no external 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).

#include <cassert>
#include <cctype>
#include <cstdio>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

namespace chmod {

/// Which permission class a triplet describes — governs which special-bit
/// marker (s/S for owner+group, t/T for other) is legal in its exec slot.
enum class pos { owner, group, other };

/// Full chmod breakdown — the C++ mirror of the TS `ChmodResult` / Go `Result`.
struct chmod_result {
    std::string octal;    ///< 4-digit zero-padded octal, e.g. "0755".
    std::string symbolic; ///< 9-char rwxrwxrwx with special markers, e.g. "rwsr-xr-x".
    int decimal;          ///< Raw integer mode (0-4095).
    bool setuid;
    bool setgid;
    bool sticky;
};

// Forward declarations — detail::build_result calls the renderers defined
// further below (kept there as the public API surface).
inline std::string mode_to_octal(int mode);
inline std::string mode_to_symbolic(int mode);

namespace detail {

/// View of `s` with ASCII whitespace trimmed from both ends (mirrors .trim()).
inline std::string_view trim(std::string_view s) {
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) s.remove_prefix(1);
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) s.remove_suffix(1);
    return s;
}

/// Parse a 3-char rwx triplet at `p`. 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)` on success, or std::nullopt if any char is invalid.
inline std::optional<std::pair<int, int>> parse_triplet(std::string_view tri, pos p) {
    if (tri.size() != 3) return std::nullopt;
    int digit = 0;
    switch (tri[0]) {
        case 'r': digit |= 4; break;
        case '-': break;
        default: return std::nullopt;
    }
    switch (tri[1]) {
        case 'w': digit |= 2; break;
        case '-': break;
        default: return std::nullopt;
    }
    int special = 0;
    switch (const char c = tri[2]) {
        case 'x': digit |= 1; break;
        case '-': break;
        case 's': case 'S':
            if (p != pos::owner && p != pos::group) return std::nullopt;
            if (c == 's') digit |= 1;
            special = p == pos::owner ? 4 : 2;
            break;
        case 't': case 'T':
            if (p != pos::other) return std::nullopt;
            if (c == 't') digit |= 1;
            special = 1;
            break;
        default: return std::nullopt;
    }
    return std::make_pair(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'.
inline std::string format_triplet(int digit, bool has_special, char marker) {
    std::string out;
    out.reserve(3);
    out.push_back(digit & 4 ? 'r' : '-');
    out.push_back(digit & 2 ? 'w' : '-');
    const bool exec = (digit & 1) != 0;
    if (has_special && exec) {
        out.push_back(marker);
    } else if (has_special) {
        out.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(marker))));
    } else if (exec) {
        out.push_back('x');
    } else {
        out.push_back('-');
    }
    return out;
}

inline chmod_result build_result(int mode) {
    const int special = (mode >> 9) & 7;
    return chmod_result{
        mode_to_octal(mode),
        mode_to_symbolic(mode),
        mode & 07777,
        (special & 4) != 0,
        (special & 2) != 0,
        (special & 1) != 0,
    };
}

} // namespace detail

/// Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or std::nullopt.
inline std::optional<int> symbolic_to_mode(std::string_view sym) {
    const std::string_view s = detail::trim(sym);
    if (s.size() != 9) return std::nullopt;
    const auto o = detail::parse_triplet(s.substr(0, 3), pos::owner);
    if (!o) return std::nullopt;
    const auto g = detail::parse_triplet(s.substr(3, 3), pos::group);
    if (!g) return std::nullopt;
    const auto ot = detail::parse_triplet(s.substr(6, 3), pos::other);
    if (!ot) return std::nullopt;
    const int special = o->second | g->second | ot->second;
    return special * 01000 /* 0o1000 */ + (o->first << 6) + (g->first << 3) + ot->first;
}

/// Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or std::nullopt.
inline std::optional<int> octal_to_mode(std::string_view octal) {
    const std::string_view s = detail::trim(octal);
    if (s.size() != 3 && s.size() != 4) return std::nullopt;
    int value = 0;
    for (const char c : s) {
        if (c < '0' || c > '7') return std::nullopt;
        value = value * 8 + (c - '0');
    }
    return value;
}

/// Render a raw mode as 9-char symbolic notation.
inline std::string mode_to_symbolic(int mode) {
    const int special = (mode >> 9) & 7;
    return detail::format_triplet((mode >> 6) & 7, (special & 4) != 0, 's') +
           detail::format_triplet((mode >> 3) & 7, (special & 2) != 0, 's') +
           detail::format_triplet(mode & 7, (special & 1) != 0, 't');
}

/// Render a raw mode as a 4-digit zero-padded octal string.
inline std::string mode_to_octal(int mode) {
    char buf[5];
    std::snprintf(buf, sizeof buf, "%04o", mode & 07777);
    return buf;
}

/// Build a full result from symbolic notation, or std::nullopt if invalid.
inline std::optional<chmod_result> from_symbolic(std::string_view sym) {
    const auto mode = symbolic_to_mode(sym);
    if (!mode) return std::nullopt;
    return detail::build_result(*mode);
}

/// Build a full result from an octal string, or std::nullopt if invalid.
inline std::optional<chmod_result> from_octal(std::string_view octal) {
    const auto mode = octal_to_mode(octal);
    if (!mode) return std::nullopt;
    return detail::build_result(*mode);
}

} // namespace chmod

// --- showcase assertions (the canonical suite lives in src/lib) ------------------
namespace {

void expect(bool condition, const char *what) {
    if (!condition) throw std::runtime_error(std::string("showcase check failed: ") + what);
}

} // namespace

int main() {
    using chmod::from_octal;
    using chmod::from_symbolic;
    using chmod::octal_to_mode;
    using chmod::symbolic_to_mode;

    const auto r = from_octal("755");
    expect(r.has_value(), "755 parses");
    expect(r->octal == "0755", "755 octal");
    expect(r->symbolic == "rwxr-xr-x", "755 symbolic");
    expect(r->decimal == 0755, "755 decimal");
    expect(!r->setuid && !r->setgid && !r->sticky, "755 no special bits");

    expect(from_symbolic("rwxr-xr-x")->octal == "0755", "symbolic round-trip");

    const auto su = from_octal("4755"); // setuid over rwxr-xr-x -> exec slot becomes 's'
    expect(su.has_value(), "4755 parses");
    expect(su->symbolic == "rwsr-xr-x", "4755 symbolic");
    expect(su->decimal == 04755, "4755 decimal");
    expect(su->setuid && !su->sticky, "4755 setuid only");

    const auto st = from_octal("1644"); // sticky over rw-r--r--, no exec -> marker 'T'
    expect(st.has_value(), "1644 parses");
    expect(st->symbolic == "rw-r--r-T", "1644 symbolic");
    expect(st->decimal == 01644, "1644 decimal");
    expect(st->sticky && !st->setuid, "1644 sticky only");

    expect(!octal_to_mode("999").has_value(), "'9' is not an octal digit");
    expect(!symbolic_to_mode("rwx").has_value(), "wrong length rejected");
    expect(from_octal("0000")->symbolic == "---------", "zero round-trip");

    std::cout << "All chmod showcase tests passed.\n";
    return 0;
}

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 →