Skip to content

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

// chmod-calculator — POSIX permission mode converter (octal <-> symbolic).
//
// Language: Swift (5.9, 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 Optionals (never traps).
//   - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
//   - Self-contained: stdlib only (no Foundation, no SwiftPM 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).

/// 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.
private enum Pos { case owner, group, other }

/// Full chmod breakdown — the Swift mirror of the TS `ChmodResult` / Go `Result`.
public struct ChmodResult: Equatable {
    /// 4-digit zero-padded octal, e.g. "0755".
    public let octal: String
    /// 9-char `rwxrwxrwx` with special markers, e.g. "rwsr-xr-x".
    public let symbolic: String
    /// Raw integer mode (0-4095).
    public let decimal: Int
    public let setuid: Bool
    public let setgid: Bool
    public let sticky: Bool
}

/// Namespace for the converter — Swift's caseless-enum idiom for pure namespacing.
public enum ChmodCalculator {
    /// 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). Returns `(digit, special)` on success, or nil if
    /// any char is invalid.
    private static func parseTriplet(_ tri: [Character], _ pos: Pos) -> (digit: Int, special: Int)? {
        guard tri.count == 3 else { return nil }
        var digit = 0
        switch tri[0] {
        case "r": digit |= 4
        case "-": break
        default: return nil
        }
        switch tri[1] {
        case "w": digit |= 2
        case "-": break
        default: return nil
        }
        var special = 0
        let c = tri[2]
        switch c {
        case "x": digit |= 1
        case "-": break
        case "s", "S":
            guard pos == .owner || pos == .group else { return nil }
            if c == "s" { digit |= 1 }
            special = pos == .owner ? 4 : 2
        case "t", "T":
            guard pos == .other else { return nil }
            if c == "t" { digit |= 1 }
            special = 1
        default: return nil
        }
        return (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".
    private static func formatTriplet(_ digit: Int, _ hasSpecial: Bool, _ marker: Character) -> String {
        var out = String()
        out.reserveCapacity(3)
        out.append(digit & 4 != 0 ? "r" : "-")
        out.append(digit & 2 != 0 ? "w" : "-")
        let exec = digit & 1 != 0
        if hasSpecial && exec {
            out.append(marker)
        } else if hasSpecial {
            out.append(marker.uppercased()) // 's' -> 'S', 't' -> 'T'
        } else if exec {
            out.append("x")
        } else {
            out.append("-")
        }
        return out
    }

    /// Whitespace trimmed from both ends — a stdlib replacement for
    /// Foundation's `trimmingCharacters(in:)`, keeping this port Foundation-free.
    private static func trim(_ s: String) -> String {
        let start = s.firstIndex { !$0.isWhitespace } ?? s.endIndex
        let end = s.lastIndex { !$0.isWhitespace }.map { s.index(after: $0) } ?? s.startIndex
        guard start <= end else { return "" }
        return String(s[start..<end])
    }

    /// Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or nil.
    public static func symbolicToMode(_ sym: String) -> Int? {
        let s = trim(sym)
        guard s.count == 9 else { return nil }
        let chars = Array(s)
        guard let o = parseTriplet(Array(chars[0..<3]), .owner),
              let g = parseTriplet(Array(chars[3..<6]), .group),
              let ot = parseTriplet(Array(chars[6..<9]), .other) else { return nil }
        let special = o.special | g.special | ot.special
        return special * 0o1000 + (o.digit << 6) + (g.digit << 3) + ot.digit
    }

    /// Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or nil.
    public static func octalToMode(_ octal: String) -> Int? {
        let s = trim(octal)
        guard s.count == 3 || s.count == 4 else { return nil }
        var value: Int = 0
        for ch in s {
            guard let d: Int = ch.wholeNumberValue, d >= 0, d <= 7 else { return nil }
            value = value * 8 + d
        }
        return value
    }

    /// Render a raw mode as 9-char symbolic notation.
    public static func modeToSymbolic(_ mode: Int) -> String {
        let 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")
    }

    /// Render a raw mode as a 4-digit zero-padded octal string. Built by
    /// hand-shift so the port stays Foundation-free (String(format:) would
    /// pull it in).
    public static func modeToOctal(_ mode: Int) -> String {
        let value = mode & 0o7777
        var out = String()
        out.reserveCapacity(4)
        for shift in [9, 6, 3, 0] {
            out.append(String((value >> shift) & 7))
        }
        return out
    }

    private static func buildResult(_ mode: Int) -> ChmodResult {
        let special = (mode >> 9) & 7
        return ChmodResult(
            octal: modeToOctal(mode),
            symbolic: modeToSymbolic(mode),
            decimal: mode & 0o7777,
            setuid: special & 4 != 0,
            setgid: special & 2 != 0,
            sticky: special & 1 != 0)
    }

    /// Build a full result from symbolic notation, or nil if invalid.
    public static func fromSymbolic(_ sym: String) -> ChmodResult? {
        guard let mode = symbolicToMode(sym) else { return nil }
        return buildResult(mode)
    }

    /// Build a full result from an octal string, or nil if invalid.
    public static func fromOctal(_ octal: String) -> ChmodResult? {
        guard let mode = octalToMode(octal) else { return nil }
        return buildResult(mode)
    }
}

// --- showcase assertions (the canonical suite lives in src/lib) ------------------
let r = ChmodCalculator.fromOctal("755")!
assert(r.octal == "0755" && r.symbolic == "rwxr-xr-x" && r.decimal == 0o755, "755 basics")
assert(!r.setuid && !r.setgid && !r.sticky, "755 no special bits")

assert(ChmodCalculator.fromSymbolic("rwxr-xr-x")!.octal == "0755", "symbolic round-trip")

let su = ChmodCalculator.fromOctal("4755")! // setuid over rwxr-xr-x -> exec slot becomes 's'
assert(su.symbolic == "rwsr-xr-x" && su.decimal == 0o4755, "4755 renders 's' marker")
assert(su.setuid && !su.sticky, "4755 setuid only")

let st = ChmodCalculator.fromOctal("1644")! // sticky over rw-r--r--, no exec -> marker 'T'
assert(st.symbolic == "rw-r--r-T" && st.decimal == 0o1644, "1644 renders 'T' marker")
assert(st.sticky && !st.setuid, "1644 sticky only")

assert(ChmodCalculator.octalToMode("999") == nil, "'9' is not an octal digit")
assert(ChmodCalculator.symbolicToMode("rwx") == nil, "wrong length rejected")
assert(ChmodCalculator.fromOctal("0000")!.symbolic == "---------", "zero round-trip")

print("All chmod showcase tests passed.")

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 →