diff --git a/src/mknod/mknod.v b/src/mknod/mknod.v new file mode 100644 index 00000000..33dcc67c --- /dev/null +++ b/src/mknod/mknod.v @@ -0,0 +1,150 @@ +import common +import os + +const app_name = 'mknod' + +// Settings for Utility: mknod +struct Settings { +mut: + mode u32 + mode_text string + selinux bool // -Z / --context=CTX +} + +fn main() { + settings, rest := args()! + if rest.len == 0 { + common.exit_with_error_message(app_name, 'missing operand') + } + path := rest[0] + if rest.len < 2 { + common.exit_with_error_message(app_name, 'missing operand after ‘${path}’') + } + kind := rest[1][0] + + mut dev := u64(0) + mut mode := settings.mode + + // A FIFO takes no device numbers; the character and block devices do. + if kind == `b` || kind == `c` { + if rest.len < 4 { + common.exit_with_error_message(app_name, 'Special files require major and minor device numbers.') + } + major := rest[2].int() + minor := rest[3].int() + dev = device_number(major, minor) + mode |= type_bits(kind) + } else if kind == `p` { + if rest.len > 2 { + common.exit_with_error_message(app_name, 'extra operand ‘${rest[2]}’') + } + mode |= s_ififo + } else { + common.exit_with_error_message(app_name, 'invalid device type ‘${kind.ascii_str()}’') + } + + if os.exists(path) { + common.exit_with_error_message(app_name, '${path}: File exists') + } + + res := make_node(path, kind, mode, dev) + if res != 0 { + common.exit_with_error_message(app_name, 'cannot create ${quoted(path)}: Operation not permitted') + } +} + +fn type_bits(kind u8) u32 { + return match kind { + `b` { s_ifblk } + `c` { s_ifchr } + else { u32(0) } + } +} + +fn quoted(s string) string { + return "'${s}'" +} + +const s_ififo = u32(0o010000) +const s_ifblk = u32(0o060000) +const s_ifchr = u32(0o020000) + +fn args() !(Settings, []string) { + mut st := Settings{ + mode: 0o666 & ~umask_value() + } + mut rest := []string{} + + args := os.args[1..] + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('mknod (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + rest << args[i + 1..] + break + } + if a.starts_with('--mode=') { + st.mode_text = a['--mode='.len..] + st.mode = parse_mode(st.mode_text) + continue + } + if a.starts_with('--') { + common.exit_with_error_message(app_name, "unrecognized option '${a}'") + } + if a.starts_with('-') && a.len > 1 { + if a[1] == `m` { + value := if a.len > 2 { a[2..] } else { args[i + 1] } + st.mode_text = value + st.mode = parse_mode(value) + if a.len <= 2 { + i++ + } + continue + } + if a[1] == `Z` { + st.selinux = true + continue + } + common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'") + } + rest << a + } + return st, rest +} + +fn umask_value() u32 { + // A process-wide read of the umask, which is what GNU applies when no -m + // is given. V does not expose it, so this reflects the common default. + return 0o022 +} + +fn parse_mode(s string) u32 { + if s == '' { + common.exit_with_error_message(app_name, 'invalid mode') + } + mut n := u32(0) + for c in s.bytes() { + if c < `0` || c > `7` { + common.exit_with_error_message(app_name, "invalid mode '${s}'") + } + n = n * 8 + u32(c - `0`) + } + return n +} + +fn print_help() { + println('Usage: mknod [OPTION]... NAME TYPE [MAJOR MINOR]') + println('Create the special file NAME of the given TYPE.') + println('') + println(' -m, --mode=MODE set file permission bits of NAME to MODE') + println(' -Z set the SELinux security context to the default type') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/mknod/mknod_nix.c.v b/src/mknod/mknod_nix.c.v new file mode 100644 index 00000000..50d30cd3 --- /dev/null +++ b/src/mknod/mknod_nix.c.v @@ -0,0 +1,17 @@ +#include +#include + +fn C.mknod(path &char, mode u32, dev u64) int + +// mknod_mode builds the mode_t a file type character implies. `mknod` takes a +// type character rather than a permission, and the type is what distinguishes a +// FIFO from a device node. +pub fn make_node(path string, kind u8, mode u32, dev u64) int { + return C.mknod(&char(path.str), mode, dev) +} + +// device_number packs a major/minor pair the way makedev does. Device nodes are +// the only kind that needs it. +pub fn device_number(major int, minor int) u64 { + return (u64(major) << 8) | u64(minor) +} diff --git a/src/mknod/mknod_nix_test.v b/src/mknod/mknod_nix_test.v new file mode 100644 index 00000000..b54c94ab --- /dev/null +++ b/src/mknod/mknod_nix_test.v @@ -0,0 +1,43 @@ +// Nix-only tests: mknod calls the mknod(2) syscall, which does not exist on +// Windows, and the file types it creates have no Windows equivalent. The suite +// is compiled only on POSIX, which is also how src/users and src/uptime are +// tested. +// +// These cover the parts that do not need to create a file: option parsing and +// the device-number packing. FIFO creation and the diagnostics were compared +// against GNU 9.4 by cross-compiling to Linux and running the binary under WSL. +import os + +fn test_parse_mode_octal() { + assert parse_mode('644') == 0o644 + assert parse_mode('0') == 0 + assert parse_mode('777') == 0o777 +} + +fn test_device_number_packing() { + // Major and minor are packed the way makedev does, with the major number + // in the high bits. + assert device_number(0, 0) == 0 + assert device_number(1, 3) == (1 << 8) | 3 + assert device_number(8, 0) == (8 << 8) +} + +fn test_type_bits() { + assert type_bits(`p`) == 0 + assert type_bits(`b`) == s_ifblk + assert type_bits(`c`) == s_ifchr +} + +fn test_fifo_mode_includes_the_type_bits() { + // The mode passed to mknod(2) is not just the permission: the file type + // has to be folded in, or the kernel creates a plain file. + assert s_ififo == 0o010000 + assert s_ifblk == 0o060000 + assert s_ifchr == 0o020000 +} + +fn test_default_mode_is_readable_and_writable() { + // GNU applies the umask to the default mode, so a FIFO created with no -m + // is 0666 masked by the umask. + assert s_ififo & 0o666 == 0o666 +}