Skip to content

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

// Package chmod is the Go twin of CosmoDev's src/lib/chmod.ts (dual source: the
// web lib is TypeScript, the CLI lib is Go — kept in lock-step). Pure +
// deterministic, never panics. Table-driven tests share vectors with the TS
// suite so the two implementations are held to the same contract.
package chmod

import (
	"fmt"
	"strconv"
	"strings"
)

// Result is the full chmod breakdown returned by FromSymbolic / FromOctal.
type Result struct {
	Octal    string // 4-digit zero-padded, e.g. "0755"
	Symbolic string // 9-char rwxrwxrwx with special markers, e.g. "rwsr-xr-x"
	Decimal  int    // raw integer mode (0-4095)
	Setuid   bool
	Setgid   bool
	Sticky   bool
}

type pos int

const (
	posOwner pos = iota
	posGroup
	posOther
)

// parseTriplet mirrors parseTriplet in chmod.ts: the exec slot may carry a
// special-bit marker (s/S for setuid-in-owner/setgid-in-group, t/T for sticky).
func parseTriplet(tri string, p pos) (digit, special int, ok bool) {
	if len(tri) != 3 {
		return 0, 0, false
	}
	switch tri[0] {
	case 'r':
		digit |= 4
	case '-':
	default:
		return 0, 0, false
	}
	switch tri[1] {
	case 'w':
		digit |= 2
	case '-':
	default:
		return 0, 0, false
	}
	c := tri[2]
	switch {
	case c == 'x':
		digit |= 1
	case c == '-':
	case (c == 's' || c == 'S') && (p == posOwner || p == posGroup):
		if c == 's' {
			digit |= 1
		}
		if p == posOwner {
			special = 4
		} else {
			special = 2
		}
	case (c == 't' || c == 'T') && p == posOther:
		if c == 't' {
			digit |= 1
		}
		special = 1
	default:
		return 0, 0, false
	}
	return digit, special, true
}

// formatTriplet renders a 0-7 digit + optional special bit as a 3-char triplet.
// marker is 's' (owner/group) or 't' (other); upper-cased when exec is absent.
func formatTriplet(digit int, hasSpecial bool, marker byte) string {
	t := make([]byte, 3)
	if digit&4 != 0 {
		t[0] = 'r'
	} else {
		t[0] = '-'
	}
	if digit&2 != 0 {
		t[1] = 'w'
	} else {
		t[1] = '-'
	}
	exec := digit&1 != 0
	switch {
	case hasSpecial && exec:
		t[2] = marker
	case hasSpecial:
		t[2] = marker - 32 // uppercase: 's'->'S', 't'->'T'
	case exec:
		t[2] = 'x'
	default:
		t[2] = '-'
	}
	return string(t)
}

// SymbolicToMode parses "rwxr-xr-x" into a raw mode, returning ok=false if invalid.
func SymbolicToMode(sym string) (int, bool) {
	s := strings.TrimSpace(sym)
	if len(s) != 9 {
		return 0, false
	}
	od, os_, oko := parseTriplet(s[0:3], posOwner)
	gd, gs, okg := parseTriplet(s[3:6], posGroup)
	otd, ots, okot := parseTriplet(s[6:9], posOther)
	if !oko || !okg || !okot {
		return 0, false
	}
	special := os_ | gs | ots
	return special*0o1000 + (od << 6) + (gd << 3) + otd, true
}

// OctalToMode parses "755" / "4755" into a raw mode, returning ok=false if invalid.
func OctalToMode(octal string) (int, bool) {
	s := strings.TrimSpace(octal)
	if len(s) != 3 && len(s) != 4 {
		return 0, false
	}
	for _, c := range s {
		if c < '0' || c > '7' {
			return 0, false
		}
	}
	n, err := strconv.ParseInt(s, 8, 32)
	if err != nil {
		return 0, false
	}
	return int(n), true
}

// ModeToSymbolic renders a raw mode as 9-char symbolic notation.
func ModeToSymbolic(mode int) string {
	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')
}

// ModeToOctal renders a raw mode as a 4-digit zero-padded octal string.
func ModeToOctal(mode int) string {
	return fmt.Sprintf("%04o", mode&0o7777)
}

func buildResult(mode int) Result {
	special := (mode >> 9) & 7
	return Result{
		Octal:    ModeToOctal(mode),
		Symbolic: ModeToSymbolic(mode),
		Decimal:  mode & 0o7777,
		Setuid:   special&4 != 0,
		Setgid:   special&2 != 0,
		Sticky:   special&1 != 0,
	}
}

// FromSymbolic builds a Result from symbolic notation.
func FromSymbolic(sym string) (Result, bool) {
	mode, ok := SymbolicToMode(sym)
	if !ok {
		return Result{}, false
	}
	return buildResult(mode), true
}

// FromOctal builds a Result from an octal string.
func FromOctal(octal string) (Result, bool) {
	mode, ok := OctalToMode(octal)
	if !ok {
		return Result{}, false
	}
	return buildResult(mode), true
}

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 →