chmod Calculator — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// chmod-calculator — POSIX permission mode converter (octal <-> symbolic).
//
// Language: Kotlin (1.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 null (never throws).
// - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
// - Self-contained: stdlib only (no external 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).
//
// Note: Kotlin has no octal literals (unlike TS/Go/Python), so the 0o1000 /
// 0o7777 weights of the references are written as decimal 512 / 4095.
package org.cosmolabs.cosmodev.chmod
/** Full chmod breakdown — the Kotlin mirror of the TS `ChmodResult` / Go `Result`. */
data class ChmodResult(
val octal: String, // 4-digit zero-padded octal, e.g. "0755".
val symbolic: String, // 9-char rwxrwxrwx with special markers, e.g. "rwsr-xr-x".
val decimal: Int, // Raw integer mode (0-4095).
val setuid: Boolean,
val setgid: Boolean,
val sticky: Boolean,
)
/** 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 class Pos { OWNER, GROUP, OTHER }
/**
* 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 null if any char is invalid.
*/
private fun parseTriplet(tri: String, pos: Pos): Pair<Int, Int>? {
if (tri.length != 3) return null
var digit = 0
when (tri[0]) {
'r' -> digit = digit or 4
'-' -> {}
else -> return null
}
when (tri[1]) {
'w' -> digit = digit or 2
'-' -> {}
else -> return null
}
var special = 0
val c = tri[2]
when (c) {
'x' -> digit = digit or 1
'-' -> {}
's', 'S' -> {
if (pos != Pos.OWNER && pos != Pos.GROUP) return null
if (c == 's') digit = digit or 1
special = if (pos == Pos.OWNER) 4 else 2
}
't', 'T' -> {
if (pos != Pos.OTHER) return null
if (c == 't') digit = digit or 1
special = 1
}
else -> return null
}
return digit to 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 fun formatTriplet(digit: Int, hasSpecial: Boolean, marker: Char): String {
val sb = StringBuilder(3)
sb.append(if (digit and 4 != 0) 'r' else '-')
sb.append(if (digit and 2 != 0) 'w' else '-')
val exec = digit and 1 != 0
sb.append(
when {
hasSpecial && exec -> marker
hasSpecial -> marker.uppercaseChar()
exec -> 'x'
else -> '-'
}
)
return sb.toString()
}
/** Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or null. */
fun symbolicToMode(sym: String): Int? {
val s = sym.trim()
if (s.length != 9) return null
val o = parseTriplet(s.substring(0, 3), Pos.OWNER) ?: return null
val g = parseTriplet(s.substring(3, 6), Pos.GROUP) ?: return null
val ot = parseTriplet(s.substring(6, 9), Pos.OTHER) ?: return null
val special = o.second or g.second or ot.second
return special * 512 /* 0o1000 */ + (o.first shl 6) + (g.first shl 3) + ot.first
}
/** Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or null. */
fun octalToMode(octal: String): Int? {
val s = octal.trim()
if (s.length != 3 && s.length != 4) return null
var value = 0
for (c in s) {
if (c < '0' || c > '7') return null
value = value * 8 + (c - '0')
}
return value
}
/** Render a raw mode as 9-char symbolic notation. */
fun modeToSymbolic(mode: Int): String {
val special = (mode shr 9) and 7
return formatTriplet((mode shr 6) and 7, special and 4 != 0, 's') +
formatTriplet((mode shr 3) and 7, special and 2 != 0, 's') +
formatTriplet(mode and 7, special and 1 != 0, 't')
}
/** Render a raw mode as a 4-digit zero-padded octal string. */
fun modeToOctal(mode: Int): String = (mode and 4095 /* 0o7777 */).toString(8).padStart(4, '0')
private fun buildResult(mode: Int): ChmodResult {
val special = (mode shr 9) and 7
return ChmodResult(
octal = modeToOctal(mode),
symbolic = modeToSymbolic(mode),
decimal = mode and 4095, // 0o7777
setuid = special and 4 != 0,
setgid = special and 2 != 0,
sticky = special and 1 != 0,
)
}
/** Build a full result from symbolic notation, or null if invalid. */
fun fromSymbolic(sym: String): ChmodResult? = symbolicToMode(sym)?.let { buildResult(it) }
/** Build a full result from an octal string, or null if invalid. */
fun fromOctal(octal: String): ChmodResult? = octalToMode(octal)?.let { buildResult(it) }
// --- showcase assertions (the canonical suite lives in src/lib) ------------------
fun main() {
val r = fromOctal("755")!!
check(r.octal == "0755") { "755 octal" }
check(r.symbolic == "rwxr-xr-x") { "755 symbolic" }
check(r.decimal == 493) { "755 decimal" } // 493 = 0o755
check(!r.setuid && !r.setgid && !r.sticky) { "755 no special bits" }
check(fromSymbolic("rwxr-xr-x")!!.octal == "0755") { "symbolic round-trip" }
val su = fromOctal("4755")!! // setuid over rwxr-xr-x -> exec slot becomes 's'
check(su.symbolic == "rwsr-xr-x") { "4755 renders 's' marker" }
check(su.decimal == 2541) { "4755 decimal" } // 2541 = 0o4755
check(su.setuid && !su.sticky) { "4755 setuid only" }
val st = fromOctal("1644")!! // sticky over rw-r--r--, no exec -> marker 'T'
check(st.symbolic == "rw-r--r-T") { "1644 renders 'T' marker" }
check(st.decimal == 932) { "1644 decimal" } // 932 = 0o1644
check(st.sticky && !st.setuid) { "1644 sticky only" }
check(octalToMode("999") == null) { "'9' is not an octal digit" }
check(symbolicToMode("rwx") == null) { "wrong length rejected" }
check(fromOctal("0000")!!.symbolic == "---------") { "zero round-trip" }
println("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 →