Skip to content

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

# chmod-calculator — POSIX permission mode converter (octal <-> symbolic).
#
# Language: Ruby (3.2, standard library only — core, no gems)
# 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; returns nil on invalid input (never raises).
#   - Functionally equivalent to the TS/Go references: same inputs -> same outputs.
#   - Self-contained: core classes only (no gems).
#
# 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).

module Chmod
  # Full chmod breakdown — the Ruby mirror of the TS `ChmodResult` / Go `Result`.
  # Data (Ruby 3.2's immutable value object) mirrors the frozen dataclass of
  # python.py.
  Result = Data.define(:octal, :symbolic, :decimal, :setuid, :setgid, :sticky)

  class << self
    # Parse symbolic notation ("rwxr-xr-x") into a raw mode integer, or nil.
    def symbolic_to_mode(sym)
      s = sym.strip
      return nil if s.length != 9

      o = parse_triplet(s[0, 3], :owner)
      g = parse_triplet(s[3, 3], :group)
      ot = parse_triplet(s[6, 3], :other)
      return nil if o.nil? || g.nil? || ot.nil?

      special = o[1] | g[1] | ot[1]
      special * 0o1000 + (o[0] << 6) + (g[0] << 3) + ot[0]
    end

    # Parse a 3-4 digit octal string ("755" / "4755") into a raw mode, or nil.
    def octal_to_mode(octal)
      s = octal.strip
      return nil if s.length != 3 && s.length != 4
      return nil unless s.each_char.all? { |ch| ch.between?('0', '7') }

      s.to_i(8)
    end

    # Render a raw mode as 9-char symbolic notation.
    def mode_to_symbolic(mode)
      special = (mode >> 9) & 7
      format_triplet((mode >> 6) & 7, special & 4 != 0, 's') +
        format_triplet((mode >> 3) & 7, special & 2 != 0, 's') +
        format_triplet(mode & 7, special & 1 != 0, 't')
    end

    # Render a raw mode as a 4-digit zero-padded octal string.
    def mode_to_octal(mode)
      format('%04<value>o', value: mode & 0o7777)
    end

    # Build a full result from symbolic notation, or nil if invalid.
    def from_symbolic(sym)
      mode = symbolic_to_mode(sym)
      mode.nil? ? nil : build_result(mode)
    end

    # Build a full result from an octal string, or nil if invalid.
    def from_octal(octal)
      mode = octal_to_mode(octal)
      mode.nil? ? nil : build_result(mode)
    end

    private

    # Parse a 3-char rwx triplet at `pos` (:owner / :group / :other). 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] or nil.
    def parse_triplet(tri, pos)
      return nil if tri.length != 3

      digit = 0
      case tri[0]
      when 'r' then digit |= 4
      when '-' then nil
      else return nil
      end
      case tri[1]
      when 'w' then digit |= 2
      when '-' then nil
      else return nil
      end

      special = 0
      case tri[2]
      when 'x' then digit |= 1
      when '-' then nil
      when 's', 'S'
        return nil unless pos == :owner || pos == :group

        digit |= 1 if tri[2] == 's'
        special = pos == :owner ? 4 : 2
      when 't', 'T'
        return nil unless pos == :other

        digit |= 1 if tri[2] == 't'
        special = 1
      else
        return nil
      end
      [digit, special]
    end

    # 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'.
    def format_triplet(digit, has_special, marker)
      out = +(digit & 4 != 0 ? 'r' : '-')
      out << (digit & 2 != 0 ? 'w' : '-')
      exec = (digit & 1) != 0
      out << if has_special && exec
               marker
             elsif has_special
               marker.upcase
             elsif exec
               'x'
             else
               '-'
             end
      out
    end

    def build_result(mode)
      special = (mode >> 9) & 7
      Result.new(
        octal: mode_to_octal(mode),
        symbolic: mode_to_symbolic(mode),
        decimal: mode & 0o7777,
        setuid: special & 4 != 0,
        setgid: special & 2 != 0,
        sticky: special & 1 != 0,
      )
    end
  end
end

# --- showcase assertions (the canonical suite lives in src/lib) ------------------
if $PROGRAM_NAME == __FILE__
  r = Chmod.from_octal('755')
  raise '755 octal' unless r.octal == '0755'
  raise '755 symbolic' unless r.symbolic == 'rwxr-xr-x'
  raise '755 decimal' unless r.decimal == 0o755
  raise '755 no special bits' if r.setuid || r.setgid || r.sticky

  raise 'symbolic round-trip' unless Chmod.from_symbolic('rwxr-xr-x')&.octal == '0755'

  su = Chmod.from_octal('4755') # setuid over rwxr-xr-x -> exec slot becomes 's'
  raise "4755 renders 's' marker" unless su.symbolic == 'rwsr-xr-x'
  raise '4755 decimal' unless su.decimal == 0o4755
  raise '4755 setuid only' unless su.setuid && !su.sticky

  st = Chmod.from_octal('1644') # sticky over rw-r--r--, no exec -> marker 'T'
  raise "1644 renders 'T' marker" unless st.symbolic == 'rw-r--r-T'
  raise '1644 decimal' unless st.decimal == 0o1644
  raise '1644 sticky only' unless st.sticky && !st.setuid

  raise "'9' is not an octal digit" unless Chmod.octal_to_mode('999').nil?
  raise 'wrong length rejected' unless Chmod.symbolic_to_mode('rwx').nil?
  raise 'zero round-trip' unless Chmod.from_octal('0000').symbolic == '---------'

  puts 'All chmod showcase tests passed.'
end

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 →