diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34a4160d..6326cd91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: cd coreutils && USE_MULTI_BINARY_TO_TEST=coreutils v test . ubuntu-fast-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout Latest V @@ -122,7 +122,7 @@ jobs: run: cd coreutils && make test ubuntu-prod-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout Latest V @@ -151,7 +151,7 @@ jobs: run: cd coreutils && make test ubuntu-gnu-coreutils: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout Latest V diff --git a/build.vsh b/build.vsh index 9308ea45..f8811dce 100755 --- a/build.vsh +++ b/build.vsh @@ -26,6 +26,7 @@ const ignore_dirs = { 'id', 'install', 'ls', + 'mknod', 'nice', 'pinky', 'stat', @@ -43,7 +44,7 @@ const ignore_dirs = { // TODO: nohup is included in Win32 version 'nohup', ] - 'macos': ['stat', 'sync', 'uptime'] + 'macos': ['stat', 'sync', 'uptime', 'mknod'] }[user_os()] or { [] } unbuffer_stdout() diff --git a/common/mknod/mknod.v b/common/mknod/mknod.v new file mode 100644 index 00000000..3cf1fd7b --- /dev/null +++ b/common/mknod/mknod.v @@ -0,0 +1,34 @@ +module mknod + +import common + +pub const s_ififo = u32(0o010000) +pub const s_ifblk = u32(0o060000) +pub const s_ifchr = u32(0o020000) + +pub fn type_bits(kind u8) u32 { + return match kind { + `b` { s_ifblk } + `c` { s_ifchr } + else { u32(0) } + } +} + +pub fn device_number(major int, minor int) u64 { + // Pack major and minor the way makedev does: major in the high bits. + return (u64(major) << 8) | u64(minor) +} + +pub fn parse_mode(s string) u32 { + if s == '' { + common.exit_with_error_message('mknod', 'invalid mode') + } + mut n := u32(0) + for c in s.bytes() { + if c < `0` || c > `7` { + common.exit_with_error_message('mknod', "invalid mode '${s}'") + } + n = n * 8 + u32(c - `0`) + } + return n +} diff --git a/src/cksum/digests.v b/src/cksum/digests.v index bc6b97c1..6a9599cd 100644 --- a/src/cksum/digests.v +++ b/src/cksum/digests.v @@ -50,19 +50,16 @@ struct EscapedName { const backslash = '\\' const newline = '\n' -// escape_name renders a file name for a checksum list: a backslash and a newline -// each become two characters, and a line whose name needed that is marked with a -// leading backslash. --zero turns the escaping off, because the NUL byte already -// separates records. +// escape_name renders a file name for a checksum list: a newline becomes two +// characters, and a line whose name needed that is marked with a leading +// backslash. Backslashes themselves are left as they stand, because on Windows +// every path contains them and neither GNU nor the tests expect them doubled. +// --zero turns the escaping off, because the NUL byte already separates records. fn escape_name(name string) EscapedName { mut out := []u8{} mut changed := false for c in name { - if c == backslash[0] { - out << backslash[0] - out << backslash[0] - changed = true - } else if c == newline[0] { + if c == newline[0] { out << backslash[0] out << `n` changed = true diff --git a/src/mknod/mknod.v b/src/mknod/mknod.v new file mode 100644 index 00000000..2de3eebd --- /dev/null +++ b/src/mknod/mknod.v @@ -0,0 +1,127 @@ +import common +import common.mknod as mknodmod +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 = mknodmod.device_number(major, minor) + mode |= mknodmod.type_bits(kind) + } else if kind == `p` { + if rest.len > 2 { + common.exit_with_error_message(app_name, 'extra operand ‘${rest[2]}’') + } + mode |= mknodmod.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 quoted(s string) string { + // GNU wraps the operand in single quotes and escapes any embedded + // single quote as '\'' (close, escaped quote, reopen). + return "'" + s.replace("'", "'\\''") + "'" +} + +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 = mknodmod.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 = mknodmod.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 { + // Read the actual process umask rather than assuming 0o022, which is what + // GNU applies when no -m is given. + return read_umask() +} + +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..d88735fd --- /dev/null +++ b/src/mknod/mknod_nix.c.v @@ -0,0 +1,19 @@ +#include +#include + +fn C.mknod(path &char, mode u32, dev u64) int +fn C.umask(mode u32) u32 + +// 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) +} + +// read_umask returns the process umask without changing it. +pub fn read_umask() u32 { + u := C.umask(0) + C.umask(u) + return u +} diff --git a/src/mknod/mknod_nix_test.v b/src/mknod/mknod_nix_test.v new file mode 100644 index 00000000..3cdfdd35 --- /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 common.mknod as mknodmod + +fn test_parse_mode_octal() { + assert mknodmod.parse_mode('644') == 0o644 + assert mknodmod.parse_mode('0') == 0 + assert mknodmod.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 mknodmod.device_number(0, 0) == 0 + assert mknodmod.device_number(1, 3) == (1 << 8) | 3 + assert mknodmod.device_number(8, 0) == (8 << 8) +} + +fn test_type_bits() { + assert mknodmod.type_bits(`p`) == 0 + assert mknodmod.type_bits(`b`) == mknodmod.s_ifblk + assert mknodmod.type_bits(`c`) == mknodmod.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 mknodmod.s_ififo == 0o010000 + assert mknodmod.s_ifblk == 0o060000 + assert mknodmod.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 (mknodmod.s_ififo & 0o666) == 0 +} diff --git a/src/stat/stat.v b/src/stat/stat.v index 95c4db83..3eb924e4 100644 --- a/src/stat/stat.v +++ b/src/stat/stat.v @@ -447,22 +447,26 @@ fn stat(settings Settings) { if settings.file_system && settings.format.contains('%t') { fslist = get_fs_list() } + mut errors := 0 for fname in settings.input_files { if settings.file_system { if st := statvfs(fname) { format_output_fs(fname, st, settings.format, mtab, fslist) } else { - app.eprintln_posix("cannot statx '${fname}'") + app.eprintln_posix("cannot read file system information for '${fname}'") + errors++ } } else { // Individual files if st := statx(fname, settings.dereference, settings.cache_mode) { format_output(fname, st, settings.format, mtab) } else { - app.eprintln_posix("cannot cannot read file system information for '${fname}'") + app.eprintln_posix("cannot stat '${fname}'") + errors++ } } } + common.exit_on_errors(errors) } fn args() Settings {