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# 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 →