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# 12 / .NET 8, standard library only — the BCL, no NuGet)
// 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 null (never throws).
// - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
// - Self-contained: BCL only (no NuGet 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).
using System;
namespace CosmoDev.ChmodCalculator;
/// <summary>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.</summary>
internal enum Pos { Owner, Group, Other }
/// <summary>Full chmod breakdown — the C# mirror of the TS <c>ChmodResult</c>
/// / Go <c>Result</c> structs.</summary>
public sealed record ChmodResult(
string Octal, // 4-digit zero-padded octal, e.g. "0755".
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);
public static class Chmod
{
/// <summary>Parse a 3-char rwx triplet at <paramref name="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) on success,
/// or null if any char is invalid.</summary>
private static (int Digit, int Special)? ParseTriplet(ReadOnlySpan<char> tri, Pos pos)
{
if (tri.Length != 3) return null;
int digit = 0;
switch (tri[0])
{
case 'r': digit |= 4; break;
case '-': break;
default: return null;
}
switch (tri[1])
{
case 'w': digit |= 2; break;
case '-': break;
default: return null;
}
int special = 0;
char c = tri[2];
switch (c)
{
case 'x': digit |= 1; break;
case '-': break;
case 's' or 'S' when pos is Pos.Owner or Pos.Group:
if (c == 's') digit |= 1;
special = pos == Pos.Owner ? 4 : 2;
break;
case 't' or 'T' when pos is Pos.Other:
if (c == 't') digit |= 1;
special = 1;
break;
default: return null;
}
return (digit, special);
}
/// <summary>Render a 0-7 digit + optional special bit as a 3-char triplet.
/// <paramref name="marker"/> is 's' (owner/group) or 't' (other);
/// upper-cased when the exec bit is absent — yielding 'S' / 'T'.</summary>
private static string FormatTriplet(int digit, bool hasSpecial, char marker)
{
var s = new System.Text.StringBuilder(3);
s.Append((digit & 4) != 0 ? 'r' : '-');
s.Append((digit & 2) != 0 ? 'w' : '-');
bool exec = (digit & 1) != 0;
if (hasSpecial && exec) s.Append(marker);
else if (hasSpecial) s.Append(char.ToUpperInvariant(marker));
else if (exec) s.Append('x');
else s.Append('-');
return s.ToString();
}
/// <summary>Parse symbolic notation ("rwxr-xr-x") into a raw mode integer,
/// or null if invalid.</summary>
public static int? SymbolicToMode(string sym)
{
ReadOnlySpan<char> s = sym.AsSpan().Trim();
if (s.Length != 9) return null;
var o = ParseTriplet(s[..3], Pos.Owner);
if (o is null) return null;
var g = ParseTriplet(s[3..6], Pos.Group);
if (g is null) return null;
var ot = ParseTriplet(s[6..9], Pos.Other);
if (ot is null) return null;
int special = o.Value.Special | g.Value.Special | ot.Value.Special;
return special * 0o1000 + (o.Value.Digit << 6) + (g.Value.Digit << 3) + ot.Value.Digit;
}
/// <summary>Parse a 3-4 digit octal string ("755" / "4755") into a raw
/// mode, or null if invalid.</summary>
public static int? OctalToMode(string octal)
{
ReadOnlySpan<char> s = octal.AsSpan().Trim();
if (s.Length != 3 && s.Length != 4) return null;
int value = 0;
foreach (char c in s)
{
if (c < '0' || c > '7') return null;
value = value * 8 + (c - '0');
}
return value;
}
/// <summary>Render a raw mode as 9-char symbolic notation.</summary>
public static string ModeToSymbolic(int mode)
{
int special = (mode >> 9) & 7;
return FormatTriplet((mode >> 6) & 7, (special & 4) != 0, 's')
+ FormatTriplet((mode >> 3) & 7, (special & 2) != 0, 's')
+ FormatTriplet(mode & 7, (special & 1) != 0, 't');
}
/// <summary>Render a raw mode as a 4-digit zero-padded octal string.</summary>
public static string ModeToOctal(int mode) =>
Convert.ToString(mode & 0o7777, toBase: 8).PadLeft(4, '0');
private static ChmodResult BuildResult(int mode)
{
int special = (mode >> 9) & 7;
return new ChmodResult(
Octal: ModeToOctal(mode),
Symbolic: ModeToSymbolic(mode),
Decimal: mode & 0o7777,
Setuid: (special & 4) != 0,
Setgid: (special & 2) != 0,
Sticky: (special & 1) != 0);
}
/// <summary>Build a full result from symbolic notation, or null if invalid.</summary>
public static ChmodResult? FromSymbolic(string sym)
{
int? mode = SymbolicToMode(sym);
return mode is null ? null : BuildResult(mode.Value);
}
/// <summary>Build a full result from an octal string, or null if invalid.</summary>
public static ChmodResult? FromOctal(string octal)
{
int? mode = OctalToMode(octal);
return mode is null ? null : BuildResult(mode.Value);
}
// --- showcase assertions (the canonical suite lives in src/lib) ------------------
private static void Verify(bool condition, string what)
{
if (!condition) throw new InvalidOperationException($"showcase check failed: {what}");
}
public static int Main()
{
var r = FromOctal("755")!;
Verify(r.Octal == "0755" && r.Symbolic == "rwxr-xr-x" && r.Decimal == 0o755, "755 basics");
Verify(!r.Setuid && !r.Setgid && !r.Sticky, "755 no special bits");
Verify(FromSymbolic("rwxr-xr-x")!.Octal == "0755", "symbolic round-trip");
var su = FromOctal("4755")!; // setuid over rwxr-xr-x -> exec slot becomes 's'
Verify(su.Symbolic == "rwsr-xr-x" && su.Decimal == 0o4755, "4755 renders 's' marker");
Verify(su.Setuid && !su.Sticky, "4755 setuid only");
var st = FromOctal("1644")!; // sticky over rw-r--r--, no exec -> marker 'T'
Verify(st.Symbolic == "rw-r--r-T" && st.Decimal == 0o1644, "1644 renders 'T' marker");
Verify(st.Sticky && !st.Setuid, "1644 sticky only");
Verify(OctalToMode("999") is null, "'9' is not an octal digit");
Verify(SymbolicToMode("rwx") is null, "wrong length rejected");
Verify(FromOctal("0000")!.Symbolic == "---------", "zero round-trip");
Console.WriteLine("All chmod showcase tests passed.");
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 →