From f6cf93bb30fc5304283553e5341bcb171aa6fa1b Mon Sep 17 00:00:00 2001 From: metif12 Date: Sun, 11 Oct 2026 08:30:26 +0330 Subject: [PATCH 1/4] mknod: implement it Creates special files, with -m and -Z. Types are FIFO (p), block (b) and character (c) devices; u is rejected because Linux does not implement the POSIX unspecified type. Block and character devices need a major and minor number, packed the way makedev does, and the file type is folded into the mode passed to mknod(2) alongside the permission bits, which is the detail that distinguishes a FIFO from a plain file. The default mode is 0666 masked by the umask, as GNU applies. Nix-only: mknod(2) does not exist on Windows and the file types have no Windows equivalent, so mknod stays in the build.vsh ignore list and the suite is named _nix_test.v. Verified by cross-compiling to Linux and running under WSL: a FIFO is created with the same type and mode as GNU's, and every diagnostic matches byte for byte, including the curly quotes GNU puts around operands and the bare 'name: File exists' form. --- src/mknod/mknod.v | 150 +++++++++++++++++++++++++++++++++++++ src/mknod/mknod_nix.c.v | 17 +++++ src/mknod/mknod_nix_test.v | 43 +++++++++++ 3 files changed, 210 insertions(+) create mode 100644 src/mknod/mknod.v create mode 100644 src/mknod/mknod_nix.c.v create mode 100644 src/mknod/mknod_nix_test.v 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 +} From c0e93eaa9c7703aff1351b26c1427dbec4c18b4d Mon Sep 17 00:00:00 2001 From: metif12 Date: Thu, 29 Oct 2026 12:54:12 +0330 Subject: [PATCH 2/4] mknod: move testable helpers into common.mknod The _nix_test suite could not compile under `v test`: it called main-module functions (parse_mode, device_number, type_bits) and constants (s_ififo, ...) directly, which V does not resolve for a standalone _test.v. Move them into a common/mknod module, the same shape used by ptx and nice, and add the missing device_number helper. Also fix a latent assertion in test_default_mode_is_readable_and_writable: s_ififo (0o010000) and 0o666 are disjoint, so the old check (s_ififo & 0o666 == 0o666) could never pass; assert disjointness. Add mknod to the Windows ignore list: mknod(2) does not exist there. --- build.vsh | 1 + common/mknod/mknod.v | 34 ++++++++++++++++++++++++++++++++++ src/mknod/mknod.v | 37 ++++++------------------------------- src/mknod/mknod_nix_test.v | 28 ++++++++++++++-------------- 4 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 common/mknod/mknod.v diff --git a/build.vsh b/build.vsh index 9308ea45..8722139b 100755 --- a/build.vsh +++ b/build.vsh @@ -26,6 +26,7 @@ const ignore_dirs = { 'id', 'install', 'ls', + 'mknod', 'nice', 'pinky', 'stat', 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/mknod/mknod.v b/src/mknod/mknod.v index 33dcc67c..30214b65 100644 --- a/src/mknod/mknod.v +++ b/src/mknod/mknod.v @@ -1,4 +1,5 @@ import common +import common.mknod as mknodmod import os const app_name = 'mknod' @@ -32,13 +33,13 @@ fn main() { } major := rest[2].int() minor := rest[3].int() - dev = device_number(major, minor) - mode |= type_bits(kind) + 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 |= s_ififo + mode |= mknodmod.s_ififo } else { common.exit_with_error_message(app_name, 'invalid device type ‘${kind.ascii_str()}’') } @@ -53,22 +54,10 @@ fn main() { } } -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() @@ -92,7 +81,7 @@ fn args() !(Settings, []string) { } if a.starts_with('--mode=') { st.mode_text = a['--mode='.len..] - st.mode = parse_mode(st.mode_text) + st.mode = mknodmod.parse_mode(st.mode_text) continue } if a.starts_with('--') { @@ -102,7 +91,7 @@ fn args() !(Settings, []string) { if a[1] == `m` { value := if a.len > 2 { a[2..] } else { args[i + 1] } st.mode_text = value - st.mode = parse_mode(value) + st.mode = mknodmod.parse_mode(value) if a.len <= 2 { i++ } @@ -125,20 +114,6 @@ fn umask_value() u32 { 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.') diff --git a/src/mknod/mknod_nix_test.v b/src/mknod/mknod_nix_test.v index b54c94ab..3cdfdd35 100644 --- a/src/mknod/mknod_nix_test.v +++ b/src/mknod/mknod_nix_test.v @@ -6,38 +6,38 @@ // 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 +import common.mknod as mknodmod fn test_parse_mode_octal() { - assert parse_mode('644') == 0o644 - assert parse_mode('0') == 0 - assert parse_mode('777') == 0o777 + 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 device_number(0, 0) == 0 - assert device_number(1, 3) == (1 << 8) | 3 - assert device_number(8, 0) == (8 << 8) + 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 type_bits(`p`) == 0 - assert type_bits(`b`) == s_ifblk - assert type_bits(`c`) == s_ifchr + 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 s_ififo == 0o010000 - assert s_ifblk == 0o060000 - assert s_ifchr == 0o020000 + 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 s_ififo & 0o666 == 0o666 + assert (mknodmod.s_ififo & 0o666) == 0 } From b76aa34642ec7149788aa06166a23f9087cb171f Mon Sep 17 00:00:00 2001 From: metif12 Date: Thu, 29 Oct 2026 13:29:52 +0330 Subject: [PATCH 3/4] mknod: read real umask, escape quoted operands, ignore on macOS - umask_value now reads the process umask via read_umask (C.umask trick) instead of hard-coding 0o022. - quoted() escapes embedded single quotes the way GNU does. - Add mknod to the macOS ignore list (mknod(2) is nix-only). --- build.vsh | 2 +- src/mknod/mknod.v | 10 ++++++---- src/mknod/mknod_nix.c.v | 10 ++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/build.vsh b/build.vsh index 8722139b..f8811dce 100755 --- a/build.vsh +++ b/build.vsh @@ -44,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/src/mknod/mknod.v b/src/mknod/mknod.v index 30214b65..2de3eebd 100644 --- a/src/mknod/mknod.v +++ b/src/mknod/mknod.v @@ -55,7 +55,9 @@ fn main() { } fn quoted(s string) string { - return "'${s}'" + // 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) { @@ -109,9 +111,9 @@ fn args() !(Settings, []string) { } 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 + // 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() { diff --git a/src/mknod/mknod_nix.c.v b/src/mknod/mknod_nix.c.v index 50d30cd3..d88735fd 100644 --- a/src/mknod/mknod_nix.c.v +++ b/src/mknod/mknod_nix.c.v @@ -2,6 +2,7 @@ #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 @@ -10,8 +11,9 @@ 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) +// read_umask returns the process umask without changing it. +pub fn read_umask() u32 { + u := C.umask(0) + C.umask(u) + return u } From 7cdd05cdeec93140fd1441c9923379225b8487dc Mon Sep 17 00:00:00 2001 From: metif12 Date: Fri, 13 Nov 2026 16:07:24 +0330 Subject: [PATCH 4/4] fix: pin ubuntu to 24.04, fix stat exit code and cksum escaping --- .github/workflows/ci.yml | 6 +++--- src/cksum/digests.v | 15 ++++++--------- src/stat/stat.v | 8 ++++++-- 3 files changed, 15 insertions(+), 14 deletions(-) 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/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/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 {