Skip to content

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 →