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 →