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 →