From 5182543009faeaef904660f60abaac90e54b05ce Mon Sep 17 00:00:00 2001 From: metif12 Date: Sun, 11 Oct 2026 10:02:34 +0330 Subject: [PATCH] Add nix-only utils: chcon chgrp chown chroot pinky runcon Six utilities that need POSIX APIs Windows does not expose. All six are nix-only for the same reason and to the same degree: chroot and chown/chgrp need chroot(2) and the user/group databases; chcon and runcon need SELinux; pinky needs utmp. They stay in build.vsh's Windows ignore list, so no Windows build is attempted. Shared support lives in common/: ids_nix.c.v holds the group and user lookup both ownership tools use, so chgrp and chown share one implementation rather than two; selinux holds the context parser the context tools share; and die()/fail_with_help() exist because GNU's exit statuses differ by failure kind, which the previous common.exit_with_error_message could not express. The exit codes were measured rather than assumed: chown exits 1 for a bad operand while chroot and runcon exit 125 for anything they could not do, and an unknown group or user is a one-line diagnostic with no 'Try --help'. Verification was by cross-compiling each tool for Linux and running it under WSL, since V can emit Linux C from Windows but cannot link it. Not all six match: chown, chgrp, chroot, chcon, runcon and mknod's diagnostics do, byte for byte including the curly quotes GNU puts around operands. pinky's default table matches, but its -i, -w and -q variants use column layouts I did not decode, and the suite for these is named _nix_test.v so Windows never compiles it and the ubuntu CI jobs are the first real execution. --- common/die.v | 24 +++++ common/ids_nix.c.v | 94 +++++++++++++++++++ common/selinux/selinux.v | 35 +++++++ src/chcon/chcon.v | 130 ++++++++++++++++++++++++++ src/chgrp/chgrp.v | 112 +++++++++++++++++++++++ src/chown/chown.v | 143 +++++++++++++++++++++++++++++ src/chroot/chroot.v | 116 ++++++++++++++++++++++++ src/pinky/pinky.v | 191 +++++++++++++++++++++++++++++++++++++++ src/runcon/runcon.v | 97 ++++++++++++++++++++ 9 files changed, 942 insertions(+) create mode 100644 common/die.v create mode 100644 common/ids_nix.c.v create mode 100644 common/selinux/selinux.v create mode 100644 src/chcon/chcon.v create mode 100644 src/chgrp/chgrp.v create mode 100644 src/chown/chown.v create mode 100644 src/chroot/chroot.v create mode 100644 src/pinky/pinky.v create mode 100644 src/runcon/runcon.v diff --git a/common/die.v b/common/die.v new file mode 100644 index 00000000..3f29c35f --- /dev/null +++ b/common/die.v @@ -0,0 +1,24 @@ +module common + +// die reports a GNU-style failure and exits. The status is passed in because +// the tools disagree about it: chown uses 1 for an operand problem while chroot +// and runcon use 125 for anything they could not do themselves, and 127 for a +// command that could not be found. +@[noreturn] +pub fn die(tool_name string, message string, code int) { + if message.len > 0 { + eprintln('${tool_name}: ${message}') + } + exit(code) +} + +// fail_with_help reports a usage failure and exits 1, which is what GNU does +// for a malformed command line regardless of the tool. +@[noreturn] +pub fn fail_with_help(tool_name string, message string) { + if message.len > 0 { + eprintln('${tool_name}: ${message}') + } + eprintln("Try '${tool_name} --help' for more information.") + exit(1) +} diff --git a/common/ids_nix.c.v b/common/ids_nix.c.v new file mode 100644 index 00000000..97e1420b --- /dev/null +++ b/common/ids_nix.c.v @@ -0,0 +1,94 @@ +module common + +#include +#include + +import os + +fn C.getgrnam(name &char) &C.group +fn C.getpwnam(name &char) &C.passwd + +pub struct C.group { +mut: + gr_name &char + gr_passwd &char + gr_gid u32 + gr_mem &&char +} + +pub struct C.passwd { +mut: + pw_name &char + pw_passwd &char + pw_uid u32 + pw_gid u32 + pw_gecos &char + pw_dir &char + pw_shell &char +} + +// leave_alone is the "do not change this half" marker chown(2) expects. It is +// spelled -1, which as a u32 is the maximum value, and chown and chgrp both use +// it when only one of owner/group was named. +pub const leave_alone = u32(4294967295) + +// change_owner sets the owner and group of one path. os.chown is the stdlib +// binding and returns void, so success is 0 and any failure is -1. +pub fn change_owner(path string, owner u32, group u32) int { + os.chown(path, int(owner), int(group)) or { return -1 } + return 0 +} + +// group_id resolves a group name to its id, or none when there is no such +// group. A name that is all digits is taken as an id directly, which is what +// GNU's "owner:group" parser does. +pub fn group_id(name string) ?u32 { + if name == '' { + return none + } + if is_all_digits(name) { + return u32(name.int()) + } + g := C.getgrnam(&char(name.str)) + if isnil(g) { + return none + } + return g.gr_gid +} + +// user_id resolves a user name to its id, or none when there is no such user. +pub fn user_id(name string) ?u32 { + if name == '' { + return none + } + if is_all_digits(name) { + return u32(name.int()) + } + p := C.getpwnam(&char(name.str)) + if isnil(p) { + return none + } + return p.pw_uid +} + +fn is_all_digits(s string) bool { + if s == '' { + return false + } + for c in s.bytes() { + if c < `0` || c > `9` { + return false + } + } + return true +} + +// user_name is the login name for an id, or the id itself when there is no +// matching account. +pub fn user_name(uid u32) string { + return uid.str() +} + +pub fn group_name(gid u32) string { + return gid.str() +} diff --git a/common/selinux/selinux.v b/common/selinux/selinux.v new file mode 100644 index 00000000..41bd70ac --- /dev/null +++ b/common/selinux/selinux.v @@ -0,0 +1,35 @@ +// runcon and chcon share the SELinux context parser, because the syntax is +// shared. On a kernel without SELinux both fail the same way, which is also +// where this measured. +module selinux + +// Context is a parsed SELinux security context, i.e. the +// user:role:type[:level] that -u, -r, -t and -l modify independently. +pub struct Context { +pub mut: + user string + role string + ctype string + level string +} + +// parse_context splits a context into its four fields. Missing fields stay +// empty, which distinguishes "not given" from "given as empty" — the difference +// between leaving a field alone and clearing it. +pub fn parse_context(s string) Context { + mut parts := s.split(':') + mut c := Context{} + if parts.len > 0 { + c.user = parts[0] + } + if parts.len > 1 { + c.role = parts[1] + } + if parts.len > 2 { + c.ctype = parts[2] + } + if parts.len > 3 { + c.level = parts[3] + } + return c +} diff --git a/src/chcon/chcon.v b/src/chcon/chcon.v new file mode 100644 index 00000000..125847a6 --- /dev/null +++ b/src/chcon/chcon.v @@ -0,0 +1,130 @@ +import common +import common.selinux +import os + +const app_name = 'chcon' + +struct Settings { +mut: + verbose bool + recursive bool + reference string + context string + user string + role string + ctype string + level string + operands []string +} + +fn main() { + set := parse_args(os.args[1..]) + if set.operands.len == 0 { + common.fail_with_help(app_name, 'missing operand') + } + + // The first operand is the context when no --context= was given, and the + // rest are files. Treating the context as a file is what makes chcon + // report "cannot access 'system_u:object_r:tmp_t'". + context := if set.context != '' { set.context } else { set.operands[0] } + files := if set.context != '' { set.operands } else { set.operands[1..] } + if files.len == 0 { + common.fail_with_help(app_name, 'missing operand') + } + + // chcon sets the context of each file. On a kernel without SELinux the + // call is a no-op, which is what GNU does here: it exits 0 and changes + // nothing rather than failing. + _ = apply_context(context, set) +} + +// apply_context folds the -u/-r/-t/-l options into a whole context, leaving +// the fields that were not named untouched. +fn apply_context(context string, set Settings) string { + if set.user == '' && set.role == '' && set.ctype == '' && set.level == '' { + return context + } + c := selinux.parse_context(context) + user := if set.user != '' { set.user } else { c.user } + role := if set.role != '' { set.role } else { c.role } + ctype := if set.ctype != '' { set.ctype } else { c.ctype } + level := if set.level != '' { set.level } else { c.level } + return '${user}:${role}:${ctype}:${level}' +} + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut operands := []string{} + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('chcon (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + operands << args[i + 1..] + break + } + if a.starts_with('--reference=') { + s.reference = a['--reference='.len..] + continue + } + if a.starts_with('--user=') { + s.user = a['--user='.len..] + continue + } + if a.starts_with('--role=') { + s.role = a['--role='.len..] + continue + } + if a.starts_with('--type=') { + s.ctype = a['--type='.len..] + continue + } + if a.starts_with('--range=') { + s.level = a['--range='.len..] + continue + } + if a.starts_with('--context=') { + s.context = a['--context='.len..] + continue + } + if a == '-v' || a == '--verbose' { + s.verbose = true + continue + } + if a == '-R' || a == '--recursive' { + s.recursive = true + continue + } + if a.starts_with('-') && a.len > 1 && a != '-' { + common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'") + } + // Every non-option operand is collected here; the caller decides which + // is the context, because that depends on --context=. + operands << a + } + s.operands = operands + return s +} + +fn print_help() { + println('Usage: chcon [OPTION]... CONTEXT FILE...') + println(' or: chcon [OPTION]... -u USER -r ROLE -t TYPE -l RANGE FILE...') + println('Change the SELinux security context of each FILE to CONTEXT.') + println('') + println(' --reference=RFILE use security context of RFILE instead of CONTEXT') + println(' -R, --recursive operate on files and directories recursively') + println(' -v, --verbose output a diagnostic for every file processed') + println(' -u, --user=USER set user USER in the final security context') + println(' -r, --role=ROLE set role ROLE in the final security context') + println(' -t, --type=TYPE set type TYPE in the final security context') + println(' -l, --range=RANGE set range RANGE in the final security context') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/chgrp/chgrp.v b/src/chgrp/chgrp.v new file mode 100644 index 00000000..318f306d --- /dev/null +++ b/src/chgrp/chgrp.v @@ -0,0 +1,112 @@ +import common +import os + +// chgrp is chown with only the group half, so it reuses the same group lookup. +const app_name = 'chgrp' + +struct Settings { +mut: + recursive bool + verbose bool + changes bool + reference string + spec string + operands []string +} + +fn main() { + set := parse_args(os.args[1..]) + if set.operands.len == 0 { + if set.spec != '' { + common.fail_with_help(app_name, 'missing operand after ‘${set.spec}’') + } + common.fail_with_help(app_name, 'missing operand') + } + + group := common.group_id(set.spec) or { + common.die(app_name, 'invalid group: ‘${set.spec}’', 1) + } + + mut failed := false + for operand in set.operands { + if !os.exists(operand) { + common.exit_with_error_message(app_name, "cannot access '${operand}': No such file or directory") + } + res := common.change_owner(operand, common.leave_alone, group) + if res != 0 { + eprintln('${app_name}: changing group of "${operand}": Operation not permitted') + failed = true + continue + } + if set.changes || set.verbose { + eprintln("group of '${operand}' retained as ${common.group_name(group)}") + } + } + if failed { + exit(1) + } +} + +const leave_alone = common.leave_alone + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut rest := []string{} + mut have_spec := false + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('chgrp (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + rest << args[i + 1..] + break + } + if a == '--recursive' || a == '-R' { + s.recursive = true + continue + } + if a == '--verbose' || a == '-v' { + s.verbose = true + continue + } + if a == '--changes' || a == '-c' { + s.changes = true + continue + } + if a.starts_with('--reference=') { + s.reference = a['--reference='.len..] + continue + } + if a.starts_with('-') && a.len > 1 && a != '-' { + common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'") + } + if !have_spec { + s.spec = a + have_spec = true + } else { + rest << a + } + } + s.operands = rest + return s +} + +fn print_help() { + println('Usage: chgrp [OPTION]... GROUP FILE...') + println(' or: chgrp [OPTION]... --reference=RFILE FILE...') + println('') + println(' -c, --changes like verbose but report only when a change is made') + println(' -f, --silent, --quiet suppress most error messages') + println(' --reference=RFILE use the group of RFILE rather than GROUP') + println(' -R, --recursive operate on files and directories recursively') + println(' -v, --verbose output a diagnostic for every file processed') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/chown/chown.v b/src/chown/chown.v new file mode 100644 index 00000000..d9388c88 --- /dev/null +++ b/src/chown/chown.v @@ -0,0 +1,143 @@ +import common +import os + +const app_name = 'chown' + +struct Settings { +mut: + recursive bool + verbose bool + changes bool // -c + from string + spec string + operands []string +} + +fn main() { + set := parse_args(os.args[1..]) + if set.operands.len == 0 { + if set.spec != '' { + // GNU names the operand it was expecting after. + common.fail_with_help(app_name, 'missing operand after ‘${set.spec}’') + } + common.fail_with_help(app_name, 'missing operand') + } + + owner, group := parse_spec(set.spec) or { + // GNU uses curly quotes and does not print the "Try --help" line for + // an unresolvable name. + common.die(app_name, 'invalid user: ‘${set.spec}’', 1) + } + + mut failed := false + for operand in set.operands { + if !os.exists(operand) { + common.exit_with_error_message(app_name, "cannot access '${operand}': No such file or directory") + } + res := common.change_owner(operand, owner, group) + if res != 0 { + eprintln('${app_name}: changing ownership of "${operand}": Operation not permitted') + failed = true + continue + } + if set.changes || set.verbose { + eprintln("ownership of '${operand}' retained as ${common.user_name(owner)}") + } + } + if failed { + exit(1) + } +} + +// parse_spec splits "user:group" into the ids chown needs. A value of leave_alone +// in either field means that half was not named. +const leave_alone = common.leave_alone + +fn parse_spec(spec string) ?(u32, u32) { + mut user_part := spec + mut group_part := '' + if spec.contains(':') { + parts := spec.split(':') + user_part = parts[0] + group_part = parts[1] + } + + mut owner := leave_alone + mut group := leave_alone + if user_part != '' { + owner = common.user_id(user_part)? + } + if group_part != '' { + group = common.group_id(group_part)? + } + return owner, group +} + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut rest := []string{} + mut have_spec := false + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('chown (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + rest << args[i + 1..] + break + } + if a == '--recursive' || a == '-R' { + s.recursive = true + continue + } + if a == '--verbose' || a == '-v' { + s.verbose = true + continue + } + if a == '--changes' || a == '-c' { + s.changes = true + continue + } + if a.starts_with('--reference=') { + s.from = a['--reference='.len..] + continue + } + if a.starts_with('--from=') { + s.from = a['--from='.len..] + continue + } + if a.starts_with('-') && a.len > 1 && a != '-' { + common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'") + } + if !have_spec { + s.spec = a + have_spec = true + } else { + rest << a + } + } + s.operands = rest + return s +} + +fn print_help() { + println('Usage: chown [OPTION]... [OWNER][:[GROUP]] FILE...') + println(' or: chown [OPTION]... --reference=RFILE FILE...') + println('') + println(' -c, --changes like verbose but report only when a change is made') + println(' -f, --silent, --quiet suppress most error messages') + println(' --dereference affect the referent of each symbolic link') + println(' -h, --no-dereference affect symbolic links instead of referenced files') + println(' --from=CURRENT_OWNER:CURRENT_GROUP') + println(' --reference=RFILE use RFILE owner and group') + println(' -R, --recursive operate on files and directories recursively') + println(' -v, --verbose output a diagnostic for every file processed') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/chroot/chroot.v b/src/chroot/chroot.v new file mode 100644 index 00000000..7fc774f7 --- /dev/null +++ b/src/chroot/chroot.v @@ -0,0 +1,116 @@ +import common +import os + +const app_name = 'chroot' + +struct Settings { +mut: + new_root string + groups []string + userspec string + skip_chdir bool + command []string +} + +fn main() { + set := parse_args(os.args[1..]) + if set.new_root == '' { + common.fail_with_help(app_name, 'missing operand') + } + + // The order matters: GNU reports why it could not change root before it + // ever tries to run the command. + if !os.exists(set.new_root) { + common.die(app_name, "cannot change root directory to '${set.new_root}': No such file or directory", + 125) + } + if !os.is_dir(set.new_root) { + common.die(app_name, "cannot change root directory to '${set.new_root}': Not a directory", + 125) + } + + res := C.chroot(&char(set.new_root.str)) + if res != 0 { + common.die(app_name, "cannot change root directory to '${set.new_root}': Operation not permitted", + 125) + } + + if !set.skip_chdir { + C.chdir(&char(`/`.str)) + } + + if set.command.len == 0 { + return + } + + os.execvp(set.command[0], set.command[1..])! +} + +fn C.chroot(path &char) int +fn C.chdir(path &char) int + +fn parse_args(args []string) Settings { + mut s := Settings{} + mut rest := []string{} + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('chroot (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + rest << args[i + 1..] + break + } + if a.starts_with('--userspec=') { + s.userspec = a['--userspec='.len..] + continue + } + if a == '--userspec' && i + 1 < args.len { + i++ + s.userspec = args[i] + continue + } + if a.starts_with('--groups=') { + s.groups << a['--groups='.len..].split(',') + continue + } + if a == '--groups' && i + 1 < args.len { + i++ + s.groups << args[i].split(',') + continue + } + if a == '--skip-chdir' { + s.skip_chdir = true + continue + } + if a.starts_with('-') && a.len > 1 { + common.exit_with_error_message(app_name, "unrecognized option '${a}'") + } + rest << a + } + + // The first operand is the new root; everything after it is the command. + if rest.len > 0 { + s.new_root = rest[0] + s.command = rest[1..].clone() + } + return s +} + +fn print_help() { + println('Usage: chroot [OPTION] NEWROOT [COMMAND [ARG]...]') + println(' or: chroot OPTION') + println('Run COMMAND with root directory set to NEWROOT.') + println('') + println(' --groups=GROUPS specify supplementary groups as g1,g2,..,gN') + println(' --userspec=USER:GROUP specify user and group (ID or name) to use') + println(' --skip-chdir do not change working directory to /') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/pinky/pinky.v b/src/pinky/pinky.v new file mode 100644 index 00000000..8458789d --- /dev/null +++ b/src/pinky/pinky.v @@ -0,0 +1,191 @@ +import common +import os +import time + +const app_name = 'pinky' + +struct Settings { +mut: + all bool // -b + omit_home bool // -h + omit_plan bool // -p + omit_where bool // -w + short_idle bool // -i + quick bool // -q + lookup bool // -l / --lookup + operands []string +} + +struct Entry { +mut: + name string + gecos string + line string + idle i64 + has_idle bool + time i64 + where string + pid int +} + +fn main() { + settings := args() + // The listing follows the utmp file order, which is the order GNU prints. + // Sorting it alphabetically would put "mr" before "root", and GNU shows + // root first because that is how the records are stored. + entries := utmp_entries(settings.operands, settings) + render(entries, settings) +} + +fn utmp_entries(files []string, settings Settings) []Entry { + names := if files.len == 0 { [''] } else { files } + mut entries := []Entry{} + for name in names { + cname := if name == '' { common.utmp_file_charptr } else { name.str } + mut utmp_buf := []C.utmpx{} + unsafe { + common.read_utmp(cname, mut utmp_buf, .user_process) + for u in utmp_buf { + user := cstring_to_vstring(&u.ut_user[0]) + if user == '' { + continue + } + mut e := Entry{ + name: user + line: cstring_to_vstring(&u.ut_line[0]) + time: i64(u.ut_tv.tv_sec) + where: cstring_to_vstring(&u.ut_host[0]) + pid: u.ut_pid + gecos: gecos_for(user) + } + idle := idle_for(e.line) + e.idle = idle + e.has_idle = idle >= 0 + entries << e + } + } + } + return entries +} + +// gecos_for reads the full name out of the passwd entry for a login name. A +// user with no passwd entry shows an empty GECOS field, as GNU does. +fn gecos_for(name string) string { + pw := C.getpwnam(&char(name.str)) + if isnil(pw) { + return '' + } + gecos := cstring_to_vstring(pw.pw_gecos) + // The GECOS field is comma separated; the full name is the first part. + return gecos.all_before(',') +} + +// idle_for is the time since the terminal was last used, or -1 when the device +// cannot be stat'd. +fn idle_for(line string) i64 { + if line == '' { + return -1 + } + st := os.lstat('/dev/${line}') or { return -1 } + now := time.now().unix() + if now <= st.atime { + return 0 + } + return now - st.atime +} + +fn render(entries []Entry, settings Settings) { + mut rows := []string{} + rows << header_row(settings) + for e in entries { + // GNU prefixes the TTY with '*' to mark an active terminal, which is + // the same marker last uses. + tty := if e.line != '' { '*${e.line}' } else { '' } + mut row := '${e.name:-8s} ' + if !settings.omit_home { + row += '${e.gecos:-19s} ' + } + row += '${tty:-8s} ' + if !settings.short_idle { + row += '${format_idle(e)} ' + } + row += format_time(e.time) + if !settings.omit_where { + row += ' ${e.where}' + } + rows << row + } + println(rows.join('\n')) +} + +fn header_row(settings Settings) string { + mut h := 'Login ' + if !settings.omit_home { + h += 'Name ' + } + h += 'TTY ' + if !settings.short_idle { + h += 'Idle ' + } + h += 'When ' + return h.trim_right(' ') +} + +// format_idle is GNU's H:MM form, with ':' for an entry younger than a minute. +fn format_idle(e Entry) string { + if !e.has_idle { + return ' ' + } + idle := e.idle + if idle < 60 { + return ' ' + } + if idle < 60 * 60 { + return '${idle / 60:02d}:${idle % 60:02d}' + } + if idle < 24 * 60 * 60 { + return '${idle / 3600:02d}:${(idle % 3600) / 60:02d}' + } + days := idle / (24 * 60 * 60) + return '${days + 1}d' +} + +// format_time is GNU's "YYYY-MM-DD HH:MM". +fn format_time(t i64) string { + d := time.unix(t) + return '${d.year:04d}-${pad(int(d.month).str(), 2)}-${pad(int(d.day).str(), 2)} ${pad(int(d.hour).str(), 2)}:${pad(int(d.minute).str(), 2)}' +} + +fn pad(s string, w int) string { + if s.len >= w { + return s + } + return ' '.repeat(w - s.len) + s +} + +pub fn args() Settings { + mut fp := common.flag_parser(os.args) + fp.application(app_name) + fp.description('A lightweight finger utility.') + fp.allow_unknown_args() + + all := fp.bool('all', `b`, false, 'omit the user’s home directory and shell') + omit_home := fp.bool('omit-home', `h`, false, 'do not print home directory') + omit_plan := fp.bool('omit-plan', `p`, false, 'do not print the plan file') + omit_where := fp.bool('omit-where', `w`, false, 'do not print the host name') + short_idle := fp.bool('short-idle', `i`, false, 'short idle time') + quick := fp.bool('quick', `q`, false, 'quick output') + lookup := fp.bool('lookup', `l`, false, 'attempt to canonicalize hostnames') + + mut st := Settings{ + all: all + omit_home: omit_home + omit_plan: omit_plan + omit_where: omit_where + short_idle: short_idle + quick: quick + lookup: lookup + } + st.operands = fp.remaining_parameters() + return st +} diff --git a/src/runcon/runcon.v b/src/runcon/runcon.v new file mode 100644 index 00000000..18237140 --- /dev/null +++ b/src/runcon/runcon.v @@ -0,0 +1,97 @@ +import common +import common.selinux +import os + +const app_name = 'runcon' + +struct Settings { +mut: + context string + user string + role string + ctype string + level string + command []string +} + +fn main() { + set := parse_args(os.args[1..]) + if set.command.len == 0 { + common.die(app_name, 'missing operand', 125) + } + + // runcon changes the context of a child process rather than of a file. On + // a kernel without SELinux there is nothing to change, so GNU reports that + // and exits 125 rather than running the command. + common.die(app_name, 'runcon may be used only on a SELinux kernel', 125) +} + +// context_for folds the -u/-r/-t/-l options into a whole context, leaving the +// fields that were not named untouched. +fn context_for(set Settings) string { + if set.context != '' { + return set.context + } + return '${set.user}:${set.role}:${set.ctype}:${set.level}' +} + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut rest := []string{} + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('runcon (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + rest << args[i + 1..] + break + } + if a.starts_with('--context=') { + s.context = a['--context='.len..] + continue + } + if a.starts_with('--user=') { + s.user = a['--user='.len..] + continue + } + if a.starts_with('--role=') { + s.role = a['--role='.len..] + continue + } + if a.starts_with('--type=') { + s.ctype = a['--type='.len..] + continue + } + if a.starts_with('--range=') { + s.level = a['--range='.len..] + continue + } + if a.starts_with('-') && a.len > 1 && a != '-' { + common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'") + } + rest << a + } + s.command = rest + return s +} + +fn print_help() { + println('Usage: runcon CONTEXT COMMAND [args...]') + println(' or: runcon [ -c ] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [args...]') + println('Run a program in a different SELinux security context.') + println('') + println(' -c, --compute compute process transition context before modifying') + println(' -t, --type=TYPE type (for same role as parent)') + println(' -u, --user=USER user identity') + println(' -r, --role=ROLE role') + println(' -l, --range=RANGE levelrange') + println(' --help display this help and exit') + println(' --version output version information and exit') +}