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 (C11, 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 calls report failure by return value.
 *   - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
 *   - Self-contained: std only (no third-party libraries).
 *
 * 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 <assert.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

/* 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. */
typedef enum { POS_OWNER, POS_GROUP, POS_OTHER } pos_t;

/* Full chmod breakdown — the C mirror of the TS `ChmodResult` / Go `Result`
 * structs. Fixed-size buffers stay NUL-terminated so they print safely. */
typedef struct {
    char octal[5];     /* 4-digit zero-padded octal, e.g. "0755". */
    char symbolic[10]; /* 9-char rwxrwxrwx with special markers, e.g. "rwsr-xr-x". */
    int decimal;       /* Raw integer mode (0-4095). */
    bool setuid;
    bool setgid;
    bool sticky;
} chmod_result_t;

/* View of `s` with ASCII whitespace trimmed from both ends (mirrors the
 * .strip() / .trim() of the reference ports). */
static void trimmed(const char *s, const char **out, size_t *len) {
    size_t start = 0;
    size_t end = strlen(s);
    while (s[start] != '\0' && isspace((unsigned char)s[start])) start++;
    while (end > start && isspace((unsigned char)s[end - 1])) end--;
    *out = s + start;
    *len = end - start;
}

/* 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).
 * Writes the 0-7 `digit` and the 0/4/2/1 `special` weight; returns false
 * when any char is invalid. */
static bool parse_triplet(const char *tri, pos_t pos, int *digit, int *special) {
    *digit = 0;
    if (tri[0] == 'r') {
        *digit |= 4;
    } else if (tri[0] != '-') {
        return false;
    }
    if (tri[1] == 'w') {
        *digit |= 2;
    } else if (tri[1] != '-') {
        return false;
    }

    *special = 0;
    char c = tri[2];
    if (c == 'x') {
        *digit |= 1;
    } else if (c == '-') {
        /* plain absence of exec */
    } else if ((c == 's' || c == 'S') && (pos == POS_OWNER || pos == POS_GROUP)) {
        if (c == 's') *digit |= 1;
        *special = pos == POS_OWNER ? 4 : 2;
    } else if ((c == 't' || c == 'T') && pos == POS_OTHER) {
        if (c == 't') *digit |= 1;
        *special = 1;
    } else {
        return false;
    }
    return true;
}

/* Render a 0-7 digit + optional special bit as a 3-char triplet written into
 * `out` (4 bytes, NUL-terminated). `marker` is 's' (owner/group) or 't'
 * (other); upper-cased when the exec bit is absent — yielding 'S' / 'T'. */
static void format_triplet(int digit, bool has_special, char marker, char out[4]) {
    out[0] = digit & 4 ? 'r' : '-';
    out[1] = digit & 2 ? 'w' : '-';
    bool exec = (digit & 1) != 0;
    if (has_special && exec) {
        out[2] = marker;
    } else if (has_special) {
        out[2] = (char)(marker - 'a' + 'A'); /* 's'->'S', 't'->'T' */
    } else if (exec) {
        out[2] = 'x';
    } else {
        out[2] = '-';
    }
    out[3] = '\0';
}

/* Parse symbolic notation ("rwxr-xr-x") into a raw mode integer; false if invalid. */
bool symbolic_to_mode(const char *sym, int *mode) {
    const char *s;
    size_t len;
    trimmed(sym, &s, &len);
    if (len != 9) return false;

    int od, os_, gd, gs, otd, ots;
    if (!parse_triplet(s + 0, POS_OWNER, &od, &os_)) return false;
    if (!parse_triplet(s + 3, POS_GROUP, &gd, &gs)) return false;
    if (!parse_triplet(s + 6, POS_OTHER, &otd, &ots)) return false;

    int special = os_ | gs | ots;
    *mode = special * 01000 /* 0o1000 */ + (od << 6) + (gd << 3) + otd;
    return true;
}

/* Parse a 3-4 digit octal string ("755" / "4755") into a raw mode; false if invalid. */
bool octal_to_mode(const char *octal, int *mode) {
    const char *s;
    size_t len;
    trimmed(octal, &s, &len);
    if (len != 3 && len != 4) return false;

    int value = 0;
    for (size_t i = 0; i < len; i++) {
        if (s[i] < '0' || s[i] > '7') return false;
        value = value * 8 + (s[i] - '0');
    }
    *mode = value;
    return true;
}

/* Render a raw mode as 9-char symbolic notation written into `out` (10 bytes). */
void mode_to_symbolic(int mode, char out[10]) {
    int special = (mode >> 9) & 7;
    char buf[4];
    format_triplet((mode >> 6) & 7, (special & 4) != 0, 's', buf);
    memcpy(out, buf, 3);
    format_triplet((mode >> 3) & 7, (special & 2) != 0, 's', buf);
    memcpy(out + 3, buf, 3);
    format_triplet(mode & 7, (special & 1) != 0, 't', buf);
    memcpy(out + 6, buf, 3);
    out[9] = '\0';
}

/* Render a raw mode as a 4-digit zero-padded octal string written into `out` (5 bytes). */
void mode_to_octal(int mode, char out[5]) {
    snprintf(out, 5, "%04o", mode & 07777);
}

static chmod_result_t build_result(int mode) {
    chmod_result_t r;
    int special = (mode >> 9) & 7;
    mode_to_octal(mode, r.octal);
    mode_to_symbolic(mode, r.symbolic);
    r.decimal = mode & 07777;
    r.setuid = (special & 4) != 0;
    r.setgid = (special & 2) != 0;
    r.sticky = (special & 1) != 0;
    return r;
}

/* Build a full result from symbolic notation; false if invalid. */
bool from_symbolic(const char *sym, chmod_result_t *out) {
    int mode;
    if (!symbolic_to_mode(sym, &mode)) return false;
    *out = build_result(mode);
    return true;
}

/* Build a full result from an octal string; false if invalid. */
bool from_octal(const char *octal, chmod_result_t *out) {
    int mode;
    if (!octal_to_mode(octal, &mode)) return false;
    *out = build_result(mode);
    return true;
}

/* --- showcase assertions (the canonical suite lives in src/lib) ------------------ */
int main(void) {
    chmod_result_t r, su, st;

    assert(from_octal("755", &r));
    assert(strcmp(r.octal, "0755") == 0);
    assert(strcmp(r.symbolic, "rwxr-xr-x") == 0);
    assert(r.decimal == 0755);
    assert(!r.setuid && !r.setgid && !r.sticky);

    assert(from_symbolic("rwxr-xr-x", &r));
    assert(strcmp(r.octal, "0755") == 0);

    assert(from_octal("4755", &su)); /* setuid over rwxr-xr-x -> exec slot becomes 's' */
    assert(strcmp(su.symbolic, "rwsr-xr-x") == 0);
    assert(su.decimal == 04755);
    assert(su.setuid && !su.sticky);

    assert(from_octal("1644", &st)); /* sticky over rw-r--r--, no exec -> marker 'T' */
    assert(strcmp(st.symbolic, "rw-r--r-T") == 0);
    assert(st.decimal == 01644);
    assert(st.sticky && !st.setuid);

    assert(!octal_to_mode("999", &(int){0}));  /* '9' is not an octal digit */
    assert(!symbolic_to_mode("rwx", &(int){0})); /* wrong length */
    assert(from_octal("0000", &r));
    assert(strcmp(r.symbolic, "---------") == 0);

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