From 6594fc10cebb20c7c4193ab4af3a16850dbe6d05 Mon Sep 17 00:00:00 2001 From: metif12 Date: Fri, 2 Oct 2026 09:42:37 +0330 Subject: [PATCH 1/2] build: make all 76 utilities compile against current V main does not build with V 0.5.2, and `make testfmt` is red too, so every CI job has been failing. This gets both gates back to green without changing any intended behaviour. Compile fixes, one per cause: - `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut and tail. - `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted: the condition has to be `bool`. Affects cksum and expand. - `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now converts explicitly and saturates instead of overflowing, so `sleep inf` keeps waiting rather than wrapping around to roughly zero. - `const` cannot hold the result of a function call, so printenv's NUL terminator became a function. - `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared with `?`, but every failure path in them calls the @[noreturn] `common.exit_with_error_message`, so they never return an error and their callers had no `err` to look at. They now return plain tuples. - `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a local declaration resolved to that one and failed to compile. stat binds a distinct name to the libc call with #define. - `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in src/users/users.c.v with a different, shorter field list, which V now rejects as a redeclaration. Declared once, with the full layout. stat additionally needed more than a compile fix: - src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and v.pref to decode --printf escapes. Building it pulled the whole V compiler into the program: `v build` of src/stat never finished, and taking the machine down with it. The escapes are now decoded directly. - `` is a glibc internal header, so it is absent on musl based distributions (issue #145). The kernel uapi `` is used instead. - `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a V struct that was 160 bytes, so every call corrupted memory past the end of the allocation; stat segfaulted on every invocation. The C struct is now declared and used for real. `struct statvfs` has the same problem and its V mirror now matches the ABI including the trailing spare. - `$embed_file` no longer resolves inside a function body, so fstypes.txt is embedded at module scope. wc needed a fix of its own: - `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which does not exist for an option. os.File.read also reports end of file as an `os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so `wc` on an empty file would have panicked. It now returns `!FileChunk` and distinguishes exhaustion from a real read error. - `\r` was matched both as "ignore" and as whitespace, which V rejects as a duplicate case. - `byte(bool)` is no longer a valid conversion; the single-count check counts the selected options instead. Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected. With this, `make` builds all 76 utilities and `make testfmt` passes. --- common/readutmp_nix.c.v | 42 +++++----- common/testing/testrig.v | 3 +- src/base64/base64.v | 2 +- src/basenc/basenc.v | 4 +- src/cksum/cksum.v | 3 +- src/cp/helper.v | 6 +- src/cut/cut.v | 16 ++-- src/expand/expand.v | 2 +- src/expr/expr_test.v | 2 +- src/fmt/fmt.v | 6 +- src/head/head.v | 4 +- src/mv/helper.v | 6 +- src/nproc/nproc.v | 2 +- src/numfmt/options.v | 14 ++-- src/printenv/printenv.v | 8 +- src/printf/printf.v | 8 +- src/shred/config.v | 5 +- src/shuf/shuf.v | 4 +- src/sleep/sleep.v | 23 +++++- src/sort/options.v | 26 +++---- src/stat/{stat_to_vlib.v => modes.v} | 111 +++++++++++++++++++++++---- src/stat/stat.c.v | 92 +++++++++++++++++++--- src/stat/stat.v | 34 ++++---- src/tail/parse_args.v | 14 +++- src/tail/tail.v | 10 +-- src/truncate/truncate.v | 18 ++++- src/unlink/unlink.c.v | 1 + src/users/users.c.v | 18 ++--- src/users/users.v | 6 +- src/wc/wc.v | 24 +++--- 30 files changed, 358 insertions(+), 156 deletions(-) rename src/stat/{stat_to_vlib.v => modes.v} (51%) diff --git a/common/readutmp_nix.c.v b/common/readutmp_nix.c.v index 61a2ccd4..0b5b59f8 100644 --- a/common/readutmp_nix.c.v +++ b/common/readutmp_nix.c.v @@ -4,30 +4,30 @@ module common #include #include -// TODO: conflicts with `timeval` in -// -/* -pub struct C.timeval { - tv_sec i64 // Seconds. - tv_usec i64 // Microseconds. -}*/ - -// -pub struct C.timeval { -pub: - tv_sec u64 // Seconds. - tv_usec u64 // Microseconds. +// `struct utmpx` was previously declared twice, once here and once in +// src/users/users.c.v with a different (shorter) field list, which V rejects as +// a redeclaration. Declare it once, here, with the glibc layout. +// +// The fixed size fields are UT_LINESIZE/UT_NAMESIZE/UT_HOSTSIZE (32/32/256) and +// the element size has to match what getutxent() writes, because read_utmp() +// copies entries into a []C.utmpx. +pub struct C.exit_status { + et_code u16 + et_term u16 } pub struct C.utmpx { - ut_type i16 // Type of login. - ut_pid int // Process ID of login process. - ut_line [32]char // Devicename. - ut_id [4]char // Inittab ID. - ut_user [32]char // Username. - ut_host [256]char // Hostname for remote login. - ut_tv C.timeval // TODO: Declare sub struct correctly - ut_addr_v6 [4]int // Internet address of remote host. + ut_type i16 // Type of login. + ut_pid int // Process ID of login process. + ut_line [32]u8 // Devicename. + ut_id [4]u8 // Inittab ID. + ut_user [32]u8 // Username. + ut_host [256]u8 // Hostname for remote login. + ut_exit C.exit_status // Exit status of the process. + ut_session_id i32 // Session ID. + ut_tv C.timeval // Time entry. + ut_addr_v6 [4]i32 // Internet address of remote host. + __unused [20]u8 // Reserved. } // sets the name of the utmp-format file for the other utmp functions to access. diff --git a/common/testing/testrig.v b/common/testing/testrig.v index c87cceda..5206fa5c 100644 --- a/common/testing/testrig.v +++ b/common/testing/testrig.v @@ -84,8 +84,7 @@ pub fn (rig TestRig) assert_platform_util() { } else { // uptime was moved to procps-ng and may not be available in coreutils - assert - ver == 'uptime (GNU coreut' || ver == 'uptime (coreutils)' || ver == 'uptime from procps' + assert ver == 'uptime (GNU coreut' || ver == 'uptime (coreutils)' || ver == 'uptime from procps' } } } diff --git a/src/base64/base64.v b/src/base64/base64.v index 456c9a87..cd1cda29 100644 --- a/src/base64/base64.v +++ b/src/base64/base64.v @@ -159,7 +159,7 @@ fn main() { exit(1) } if wraping_opt < 0 { - eprintln('${application_name}: invalid wrap size: \'${wraping_opt}\'') + eprintln("${application_name}: invalid wrap size: '${wraping_opt}'") exit(1) } diff --git a/src/basenc/basenc.v b/src/basenc/basenc.v index b9175f70..f04a2e99 100644 --- a/src/basenc/basenc.v +++ b/src/basenc/basenc.v @@ -1,5 +1,5 @@ import os -import v.mathutil +import math import encoding.base64 import encoding.base32 @@ -51,7 +51,7 @@ fn print_encoded(encoded string, options Options) { return } for start := 0; start < encoded.len; start += options.wrap { - end := mathutil.min(start + options.wrap, encoded.len) + end := math.min(start + options.wrap, encoded.len) // safe to use string slicing because all chars // are in printable ascii println(encoded[start..end]) diff --git a/src/cksum/cksum.v b/src/cksum/cksum.v index 146aef5f..f4e4bad4 100644 --- a/src/cksum/cksum.v +++ b/src/cksum/cksum.v @@ -70,8 +70,9 @@ fn calc_sums(args Args) { } mut len_counter := total_length - for ; len_counter; len_counter >>= 8 { + for len_counter > 0 { crc = (crc << 8) ^ crctab[0][((crc >> 24) ^ len_counter) & 0xFF] + len_counter >>= 8 } crc = ~crc & 0xffff_ffff diff --git a/src/cp/helper.v b/src/cp/helper.v index 9c36db01..b259355d 100644 --- a/src/cp/helper.v +++ b/src/cp/helper.v @@ -69,7 +69,7 @@ fn success_exit(messages ...string) { exit(0) } -fn setup_cp_command(args []string) ?(CpCommand, []string, string) { +fn setup_cp_command(args []string) (CpCommand, []string, string) { mut fp := common.flag_parser(args) fp.application('cp') fp.limit_free_args_to_at_least(1) or { common.exit_with_error_message(name, err.msg()) } @@ -143,9 +143,7 @@ fn setup_cp_command(args []string) ?(CpCommand, []string, string) { } pub fn run_cp(args []string) { - cp, sources, dest := setup_cp_command(args) or { - common.exit_with_error_message(name, err.msg()) - } + cp, sources, dest := setup_cp_command(args) if sources.len > 1 && !os.is_dir(dest) { common.exit_with_error_message(name, target_not_dir(dest)) } diff --git a/src/cut/cut.v b/src/cut/cut.v index c3764386..03084132 100644 --- a/src/cut/cut.v +++ b/src/cut/cut.v @@ -3,8 +3,8 @@ import arrays import common import flag import io +import math import os -import v.mathutil const app_name = 'cut' const space_comma = ' ,' @@ -124,13 +124,13 @@ fn combine_ranges_and_zero_index(ranges []Range, max int, complement bool) []Ran outer: for range in ranges.sorted(a.start < b.start) { start := range.start - 1 - end := if range.end == -1 { max } else { mathutil.min(max, range.end) } + end := if range.end == -1 { max } else { math.min(max, range.end) } for mut combined_range in combined_ranges { if range_overlaps_range(start, end, combined_range.start, combined_range.end) { combined_range = Range{ - start: mathutil.min(start, combined_range.start) - end: mathutil.max(end, combined_range.end) + start: math.min(start, combined_range.start) + end: math.max(end, combined_range.end) } continue outer } @@ -189,7 +189,7 @@ fn get_args(args []string) Args { delimiter := fp.string('delimter', `d`, '', 'use instead of TAB for field delimter') fields := fp.string('fields', `f`, '', 'select only fields; also print any line${wrap}' + - 'that contains no delimiter character, unless the${wrap}-s option is specified') + 'that contains no delimiter character, unless the${wrap}-s option is specified') fp.bool('', `n`, false, '(ignored)') complement := fp.bool('complement', ` `, false, 'complement the set of selected bytes, characters${wrap}or fields') @@ -207,9 +207,9 @@ fn get_args(args []string) Args { 'range, or many ranges separated by commas. Selected input is written${eol}' + 'in the same order that it is read, and is written exactly once.${eol}${eol}' + 'Each range is one of:${eol}${eol}' + - ' N N\'th byte, character or field, counted from 1${eol}' + - ' N- from N\'th byte, character or field, to end of line${eol}' + - ' N-M from N\'th to M\'th (included) byte, character or field${eol}' + " -M from first to M'th (included) byte, character or field") + " N N'th byte, character or field, counted from 1${eol}" + + " N- from N'th byte, character or field, to end of line${eol}" + + " N-M from N'th to M'th (included) byte, character or field${eol}" + " -M from first to M'th (included) byte, character or field") fp.footer(common.coreutils_footer()) file_args := fp.finalize() or { exit_error(err.msg()) } diff --git a/src/expand/expand.v b/src/expand/expand.v index 0e54e215..8771e1c0 100644 --- a/src/expand/expand.v +++ b/src/expand/expand.v @@ -8,7 +8,7 @@ const nl = '\n' fn process_line(line string, initial bool, tabs int) { mut sp := '' - for i := tabs; i; i-- { + for _ in 0 .. tabs { sp += ' ' } diff --git a/src/expr/expr_test.v b/src/expr/expr_test.v index 5dd9c71b..7d15be0e 100644 --- a/src/expr/expr_test.v +++ b/src/expr/expr_test.v @@ -161,7 +161,7 @@ const mb_tests = [ // fail: multi; broken UTF-8 is treated in different way // "substr \u03B1bc\xB4ef 3 3", 'match abcdef ab', - 'match abcdef "\\(ab\\)"' + 'match abcdef "\\(ab\\)"', // fail: single; this is a good news, regex in vlib supports UTF-8 // "match \u03B1bc\u03B4ef .bc", // fail: both; broken regex of vlib, see #10885 diff --git a/src/fmt/fmt.v b/src/fmt/fmt.v index 1226fdb9..441ff77c 100644 --- a/src/fmt/fmt.v +++ b/src/fmt/fmt.v @@ -241,11 +241,11 @@ fn process_args(args []string) App { crown_marg := fp.bool('crown-margin', `c`, false, 'preserve the indentation of the first two lines within' + - '${pad}a paragraph, and align the left margin of each' + - '${pad}subsequent line with that of the second line') + '${pad}a paragraph, and align the left margin of each' + + '${pad}subsequent line with that of the second line') prefix_str := fp.string('prefix', `p`, '', 'reformat only lines beginning with STRING, reattaching ' + - '${pad}the prefix to reformatted lines') + '${pad}the prefix to reformatted lines') split_only := fp.bool('split-only', `s`, false, 'split long lines, but do not refill') tagged_par := fp.bool('tagged-paragraph', `t`, false, 'indentation of first line different from second') diff --git a/src/head/head.v b/src/head/head.v index 5c02bca4..73fe15ee 100644 --- a/src/head/head.v +++ b/src/head/head.v @@ -294,7 +294,7 @@ fn wrap_long_command_description(description string, max_cols int) string { return buf.str() } -fn setup_command(args []string) ?(HeadCommand, []InputFile) { +fn setup_command(args []string) (HeadCommand, []InputFile) { mut fp := common.flag_parser(args) fp.application(name) fp.usage_example('[OPTION]... [FILE]...') @@ -330,7 +330,7 @@ fn setup_command(args []string) ?(HeadCommand, []InputFile) { } fn run_head(args []string) { - head, mut files := setup_command(args) or { common.exit_with_error_message(name, err.msg()) } + head, mut files := setup_command(args) head.run(mut files) } diff --git a/src/mv/helper.v b/src/mv/helper.v index 4bccee39..61513532 100644 --- a/src/mv/helper.v +++ b/src/mv/helper.v @@ -48,9 +48,7 @@ fn int_yes(prompt string) bool { } pub fn run_mv(args []string) { - mv, sources, dest := setup_mv_command(args) or { - common.exit_with_error_message(name, err.msg()) - } + mv, sources, dest := setup_mv_command(args) if sources.len > 1 && !os.is_dir(dest) { common.exit_with_error_message(name, target_not_dir(dest)) } @@ -59,7 +57,7 @@ pub fn run_mv(args []string) { } } -fn setup_mv_command(args []string) ?(MvCommand, []string, string) { +fn setup_mv_command(args []string) (MvCommand, []string, string) { mut fp := common.flag_parser(args) fp.application('mv') fp.limit_free_args_to_at_least(1) or { common.exit_with_error_message(name, err.msg()) } diff --git a/src/nproc/nproc.v b/src/nproc/nproc.v index 533c78b8..4638dd74 100644 --- a/src/nproc/nproc.v +++ b/src/nproc/nproc.v @@ -25,7 +25,7 @@ fn main() { // Check if ignored CPUs is > 0 if ignored_cpus < 0 { - println('nproc: invalid number: \'${ignored_cpus}\'') + println("nproc: invalid number: '${ignored_cpus}'") exit(1) } diff --git a/src/numfmt/options.v b/src/numfmt/options.v index 4ff0d7bd..c6268fd6 100644 --- a/src/numfmt/options.v +++ b/src/numfmt/options.v @@ -45,21 +45,21 @@ fn get_options() Options { 'use locale-defined grouping of digits, e.g. 1,000,000') header := fp.int('header', 0, 1, 'print (without converting) the first N header lines; \n${flag.space}' + - 'defaults to 1 if not specified') + 'defaults to 1 if not specified') invalid := fp.string('invalid', 0, 'abort', 'failure mode for invalid numbers: MODE can be:\n${flag.space}' + - 'abort (default), fail, warn, ignore') + 'abort (default), fail, warn, ignore') padding := fp.int('padding', 0, 0, 'pad the output to N characters; positive N will\n${flag.space}' + - 'right-align; negative N will left-align; padding is\n${flag.space}' + - 'ignored if the output is wider than N; the default is to\n${flag.space}' + - 'automatically pad if a whitespace is found') + 'right-align; negative N will left-align; padding is\n${flag.space}' + + 'ignored if the output is wider than N; the default is to\n${flag.space}' + + 'automatically pad if a whitespace is found') round := fp.string('round', 0, 'from-zero', 'use METHOD for rounding when scaling; METHOD can be:\n${flag.space}' + - 'up, down, from-zero (default), towards-zero, nearest') + 'up, down, from-zero (default), towards-zero, nearest') suffix := fp.string('suffix', 0, '', 'add SUFFIX to output numbers, and accept optional SUFFIX\n${flag.space}' + - 'in input numbers') + 'in input numbers') to := fp.string('to', 0, 'none', 'auto-scale output numbers to UNIT\n${flag.space}' + 'none, si, iec, iec-i') to_unit := fp.int('to-unit', 0, 1, 'the output unit size (instead of the default 1)') diff --git a/src/printenv/printenv.v b/src/printenv/printenv.v index 05f180ce..44367c52 100644 --- a/src/printenv/printenv.v +++ b/src/printenv/printenv.v @@ -1,7 +1,9 @@ import os import common -const zero_byte = byte(0).ascii_str() +fn zero_byte() string { + return u8(0).ascii_str() +} // Exit status: // 0 if all variables specified were found @@ -34,7 +36,7 @@ fn main() { mut s := '${k}=${v}' if opt_nul_terminate { print(s) - print(zero_byte) + print(zero_byte()) } else { println(s) } @@ -49,7 +51,7 @@ fn main() { } if opt_nul_terminate { print(v) - print(zero_byte) + print(zero_byte()) } else { println(v) } diff --git a/src/printf/printf.v b/src/printf/printf.v index 58c486fc..c76ffab7 100644 --- a/src/printf/printf.v +++ b/src/printf/printf.v @@ -242,7 +242,10 @@ fn apply_posix_escape(s string) string { } } } - return if has_unprintable { '\$\'${upout}\'' } else { s.replace_each([ + return if has_unprintable { + "\$'${upout}'" + } else { + s.replace_each([ '|', '\\|', '&', @@ -269,7 +272,8 @@ fn apply_posix_escape(s string) string { '\\"', ' ', '\\ ', - ]) } + ]) + } } // A code below is modded version of vlib/strconv/vprintf.v whose parameter to be an array of string diff --git a/src/shred/config.v b/src/shred/config.v index b1a8ef4c..c9776eb0 100644 --- a/src/shred/config.v +++ b/src/shred/config.v @@ -6,7 +6,7 @@ import os @[version: '0.1'] struct Config { force bool @[short: f; xdoc: 'change permissions to allow writing if necessary'] - iterations int = 3 @[short: n; xdoc: 'overwrite N times instead of the default (3)'] + iterations int = 3 @[short: n; xdoc: 'overwrite N times instead of the default (3)'] random_source string @[xdoc: 'get random bytes from '] size string @[short: s; xdoc: 'shred this many bytes (suffixes like K, M, G accepted)\n'] rm bool @[only: u; xdoc: 'deallocate and remove file after overwriting'] @@ -26,8 +26,7 @@ fn get_args() (Config, []string) { description: 'Usage: shred [OPTION]... FILE...\n' + 'Overwrite the specified FILE(s) repeatedly, in order to make it harder\n' + 'for even very expensive hardware probing to recover the data.' - footer: - '\nDelete FILE(s) if --remove (-u) is specified. The default is not to remove\n' + 'the files because it is common to operate on device files like /dev/hda,\n' + 'and those files usually should not be removed.\n\n' + 'The --remove parameter indicates how to remove a directory entry:\n' + " 'unlink' => use a standard unlink call.\n" + " 'wipe' => also first obfuscate bytes in the name.\n" + " 'wipesync' => also sync each obfuscated byte to the device.\n" + "The default mode is 'wipesync', but note it can be expensive.\n\n" + 'CAUTION: shred assumes the file system and hardware overwrite data in place.\n' + 'Although this is common, many platforms operate otherwise. Also, backups\n' + 'and mirrors may contain unremovable copies that will let a shredded file\n' + 'be recovered later.\n' + common.coreutils_footer() + footer: '\nDelete FILE(s) if --remove (-u) is specified. The default is not to remove\n' + 'the files because it is common to operate on device files like /dev/hda,\n' + 'and those files usually should not be removed.\n\n' + 'The --remove parameter indicates how to remove a directory entry:\n' + " 'unlink' => use a standard unlink call.\n" + " 'wipe' => also first obfuscate bytes in the name.\n" + " 'wipesync' => also sync each obfuscated byte to the device.\n" + "The default mode is 'wipesync', but note it can be expensive.\n\n" + 'CAUTION: shred assumes the file system and hardware overwrite data in place.\n' + 'Although this is common, many platforms operate otherwise. Also, backups\n' + 'and mirrors may contain unremovable copies that will let a shredded file\n' + 'be recovered later.\n' + common.coreutils_footer() ) or { panic(err) } println(doc) exit(0) diff --git a/src/shuf/shuf.v b/src/shuf/shuf.v index 51ec4137..5fce1649 100644 --- a/src/shuf/shuf.v +++ b/src/shuf/shuf.v @@ -88,7 +88,7 @@ fn shuffle_lines(lines []string, settings Settings) []string { exit(1) } mut bytes := io.read_all(io.ReadAllConfig{ reader: file, read_to_end_of_stream: true }) or { - eprintln('${app_name}: ${settings.random_source}: Can\'t read file') + eprintln("${app_name}: ${settings.random_source}: Can't read file") exit(1) } mut seed := u32(0) @@ -159,7 +159,7 @@ fn register_lines_by_file(lines []string, fname string, zero_terminated bool) [] if zero_terminated { mut bytes := io.read_all(io.ReadAllConfig{ reader: file, read_to_end_of_stream: true }) or { - eprintln('${app_name}: ${fname}: Can\'t read file') + eprintln("${app_name}: ${fname}: Can't read file") exit(1) } diff --git a/src/sleep/sleep.v b/src/sleep/sleep.v index c65d9cb0..bd6ef7a9 100644 --- a/src/sleep/sleep.v +++ b/src/sleep/sleep.v @@ -6,6 +6,11 @@ import common const cmd_ns = 'sleep' +// max_duration is the largest sleep time.Duration we are willing to hand to +// time.sleep(), and max_sleep_seconds is that same value expressed in seconds. +const max_duration = time.Duration(max_i64) +const max_sleep_seconds = 9223372036.854776 + // // str="-1.8e+308", ret = -inf, endptr => NULL // str="1.8e+308", ret = +inf, endptr => NULL @@ -51,6 +56,22 @@ fn invalid_time_interval_argument(s string) string { return "${cmd_ns}: invalid time interval '${s}'" } +// seconds_to_duration converts a possibly infinite number of seconds +// into a time.Duration. A Duration is an integer nanosecond count, so +// saturate instead of overflowing: an unbounded sleep must keep waiting +// rather than wrap around to (near) zero. +fn seconds_to_duration(seconds f64) time.Duration { + nanoseconds := seconds * 1e9 + // `!(x < y)` also catches NaN and +inf, both of which must saturate + if !(nanoseconds < max_sleep_seconds) { + return max_duration + } + if nanoseconds <= 0.0 { + return 0 + } + return time.Duration(nanoseconds) +} + fn main() { mut fp := common.flag_parser(os.args) fp.application(cmd_ns) @@ -103,7 +124,7 @@ fn main() { // if seconds = +inf, it would not sleep // but original `sleep` would sleep t := time.ticks() - time.sleep(seconds * time.second) + time.sleep(seconds_to_duration(seconds)) $if trace_sleep_ticks ? { println(time.ticks() - t) } diff --git a/src/sort/options.v b/src/sort/options.v index 8819b2ec..52317bf9 100644 --- a/src/sort/options.v +++ b/src/sort/options.v @@ -40,11 +40,11 @@ fn get_options() Options { 'consider only printable characters') numeric := fp.bool('numeric-sort', `n`, false, 'Restrict the sort key to an initial numeric\n${flag.space}' + - 'string, consisting of optional characters,\n${flag.space}' + - 'optional character, and zero or\n${flag.space}' + - 'more digits, which shall be sorted by arithmetic\n${flag.space}' + - 'value. An empty digit string shall be treated as\n${flag.space}' + - 'zero. Leading zeros shall not affect ordering.') + 'string, consisting of optional characters,\n${flag.space}' + + 'optional character, and zero or\n${flag.space}' + + 'more digits, which shall be sorted by arithmetic\n${flag.space}' + + 'value. An empty digit string shall be treated as\n${flag.space}' + + 'zero. Leading zeros shall not affect ordering.') reverse := fp.bool('reverse', `r`, false, 'reverse the result of comparisons\n\nOther options:') check_diagnose := fp.bool('', `c`, false, 'check for sorted input; do not sort') @@ -78,14 +78,14 @@ fn get_options() Options { numeric: numeric reverse: reverse // other options - check_diagnose: check_diagnose - check_quiet: check_quiet - sort_keys: sort_keys - field_separator: field_separator - merge: merge - output_file: output_file - unique: unique - files: scan_files_arg(files) + check_diagnose: check_diagnose + check_quiet: check_quiet + sort_keys: sort_keys + field_separator: field_separator + merge: merge + output_file: output_file + unique: unique + files: scan_files_arg(files) } } diff --git a/src/stat/stat_to_vlib.v b/src/stat/modes.v similarity index 51% rename from src/stat/stat_to_vlib.v rename to src/stat/modes.v index 42454c1b..d1f98f99 100644 --- a/src/stat/stat_to_vlib.v +++ b/src/stat/modes.v @@ -1,6 +1,14 @@ import os -import v.scanner -import v.pref +import strings + +// This file used to be called `stat_to_vlib.v`: it was a staging copy of the +// file mode helpers on their way into V's os module. The types have since +// landed in vlib/os, so it is no longer a copy of anything. +// +// It also used to import v.scanner/v.pref and decode --printf escapes with the +// V language scanner. Pulling the compiler into a coreutils binary made `v +// build` of this directory take minutes (it effectively hung), so the escape +// handling below is a plain hand-written decoder instead. pub enum FileType { unknown @@ -33,13 +41,6 @@ pub: special bool // setuid for owner, setgid for group, sticky for others } -pub enum FilePermissionGroup { - unknown - owner - group - world -} - @[inline] fn if_str(cond bool, str_if_true string, str_if_false string) string { return if cond { str_if_true } else { str_if_false } @@ -107,13 +108,93 @@ fn filetype_to_string(typ FileType) string { } } -// raw_to_printf_string converts a raw string into a printf-able string -// example: r"\t[\x76]" => "\t[\x76]" = " [v]" +// raw_to_printf_string decodes the backslash escapes accepted by GNU stat's +// --printf. Example: r"\t[\x76]" becomes " [v]". +// +// Recognised: \a \b \e \f \n \r \t \v \\ \" plus \NNN (1-3 octal digits) and +// \xHH (1-2 hex digits). Anything else is passed through as the escaped +// character, with a warning, which is what GNU does. fn raw_to_printf_string(raw_string string) string { - mut sc := scanner.new_scanner("'${raw_string}'", .skip_comments, &pref.Preferences{}) - return sc.scan().lit.replace(r'\a', '\a').replace(r'\b', '\b').replace(r'\e', '\e').replace(r'\f', - '\f').replace(r'\n', '\n').replace(r'\r', '\r').replace(r'\t', '\t').replace(r'\v', '\v').replace(r'\\', - '\\').replace(r"\'", "'").replace(r'\"', '"').replace(r'\?', '\?') + mut sb := strings.new_builder(raw_string.len) + mut i := 0 + for i < raw_string.len { + c := raw_string[i] + if c != `\\` { + sb.write_byte(c) + i++ + continue + } + i++ + if i == raw_string.len { + eprintln('stat: warning: backslash at end of format') + sb.write_byte(`\\`) + break + } + e := raw_string[i] + i++ + match e { + `a` { sb.write_byte(7) } + `b` { sb.write_byte(8) } + `e` { sb.write_byte(27) } + `f` { sb.write_byte(12) } + `n` { sb.write_byte(10) } + `r` { sb.write_byte(13) } + `t` { sb.write_byte(9) } + `v` { sb.write_byte(11) } + `\\` { sb.write_byte(`\\`) } + `"` { sb.write_byte(`"`) } + `0`...`7` { + // \NNN: up to three octal digits + mut value := int(e - `0`) + mut digits := 1 + for digits < 3 && i < raw_string.len && is_octal_digit(raw_string[i]) { + value = value * 8 + int(raw_string[i] - `0`) + i++ + digits++ + } + sb.write_byte(u8(value)) + } + `x` { + // \xHH: up to two hex digits + mut value := 0 + mut digits := 0 + for digits < 2 && i < raw_string.len { + d := hex_digit_value(raw_string[i]) or { break } + value = value * 16 + d + i++ + digits++ + } + if digits == 0 { + eprintln("stat: warning: unrecognized escape '\\x'") + sb.write_byte(`x`) + } else { + sb.write_byte(u8(value)) + } + } + else { + eprintln("stat: warning: unrecognized escape '\\${e}'") + sb.write_byte(e) + } + } + } + return sb.str() +} + +fn is_octal_digit(c u8) bool { + return c >= `0` && c <= `7` +} + +fn hex_digit_value(c u8) !int { + if c >= `0` && c <= `9` { + return int(c - `0`) + } + if c >= `a` && c <= `f` { + return int(c - `a`) + 10 + } + if c >= `A` && c <= `F` { + return int(c - `A`) + 10 + } + return error('not a hex digit') } fn filemode_to_string(mode u16) string { diff --git a/src/stat/stat.c.v b/src/stat/stat.c.v index 0192010b..dfee2a94 100644 --- a/src/stat/stat.c.v +++ b/src/stat/stat.c.v @@ -4,11 +4,55 @@ import os #include #include #include -#include +// The kernel uapi definitions of statx()/STATX_* live here. Do not use +// for that: it is a glibc internal header, so it is missing on +// musl based distributions (see issue #145). +#include + +// vlib/builtin/cfns.c.v already declares a private `fn C.statvfs`, so a +// `fn C.statvfs` here resolves to that one and fails to compile. Give the libc +// call a distinct V-level name instead. +#define vcu_statvfs statvfs + +// Mirror the kernel's `struct statx` exactly. Passing a V struct through a +// voidptr and hoping for the best truncates the kernel's 224 byte write to the +// size of the V view, which corrupts memory. +struct C.statx_timestamp { + tv_sec i64 + tv_nsec u32 + reserved i32 +} // Ref: https://www.man7.org/linux/man-pages/man2/statx.2.html -fn C.statx(int, &char, int, u32, voidptr) int -fn C.statvfs(&char, voidptr) int +struct C.statx { + stx_mask u32 + stx_blksize u32 + stx_attributes u64 + stx_nlink u32 + stx_uid u32 + stx_gid u32 + stx_mode u16 + __spare0 [1]u16 + stx_ino u64 + stx_size u64 + stx_blocks u64 + stx_attributes_mask u64 + stx_atime C.statx_timestamp + stx_btime C.statx_timestamp + stx_ctime C.statx_timestamp + stx_mtime C.statx_timestamp + stx_rdev_major u32 + stx_rdev_minor u32 + stx_dev_major u32 + stx_dev_minor u32 + stx_mnt_id u64 + stx_dio_mem_align u32 + stx_dio_offset_align u32 + __spare3 [12]u32 +} + +fn C.statx(int, &char, int, u32, &C.statx) int +fn C.vcu_statvfs(&char, voidptr) int fn C.readlink(pathname &char, buf &char, bufsiz usize) int const c_at_statx_sync_as_stat = 0x0000 // C.AT_STATX_SYNC_AS_STAT from fcntl.h @@ -18,8 +62,7 @@ const c_at_symlink_nofollow = 0x0100 // C.AT_SYMLINK_NOFOLLOW from fcntl.h const c_chmod_bits = C.S_ISUID | C.S_ISGID | C.S_ISVTX | C.S_IRWXU | C.S_IRWXG | C.S_IRWXO fn statx(path string, dereference bool, cache_mode CacheMode) !Statx { - mut s := Statx{} - ptr := voidptr(&s) + mut c := C.statx{} unsafe { symlink_flag := if dereference { 0 } else { c_at_symlink_nofollow } sync_flag := match cache_mode { @@ -29,19 +72,50 @@ fn statx(path string, dereference bool, cache_mode CacheMode) !Statx { } res := C.statx(0, os.abs_path(path).str, sync_flag | symlink_flag, - C.STATX_BASIC_STATS | C.STATX_BTIME, ptr) + C.STATX_BASIC_STATS | C.STATX_BTIME, &c) if res != 0 { return os.error_posix() } } - return s + return Statx{ + stx_mask: c.stx_mask + stx_blksize: c.stx_blksize + stx_attributes: c.stx_attributes + stx_nlink: c.stx_nlink + stx_uid: c.stx_uid + stx_gid: c.stx_gid + stx_mode: c.stx_mode + stx_ino: c.stx_ino + stx_size: c.stx_size + stx_blocks: c.stx_blocks + stx_attributes_mask: c.stx_attributes_mask + stx_atime: to_timestamp(c.stx_atime) + stx_btime: to_timestamp(c.stx_btime) + stx_ctime: to_timestamp(c.stx_ctime) + stx_mtime: to_timestamp(c.stx_mtime) + stx_rdev_major: c.stx_rdev_major + stx_rdev_minor: c.stx_rdev_minor + stx_dev_major: c.stx_dev_major + stx_dev_minor: c.stx_dev_minor + stx_mnt_id: c.stx_mnt_id + stx_dio_mem_align: c.stx_dio_mem_align + stx_dio_offset_align: c.stx_dio_offset_align + } +} + +fn to_timestamp(ts C.statx_timestamp) StatxTimestamp { + return StatxTimestamp{ + tv_sec: ts.tv_sec + tv_nsec: ts.tv_nsec + } } +// statvfs() passes the V Statvfs struct through a voidptr, so that struct has +// to match the kernel's `struct statvfs` exactly, trailing spare included. fn statvfs(path string) !Statvfs { mut s := Statvfs{} - ptr := voidptr(&s) unsafe { - res := C.statvfs(os.abs_path(path).str, ptr) + res := C.vcu_statvfs(os.abs_path(path).str, voidptr(&s)) if res != 0 { return os.error_posix() } diff --git a/src/stat/stat.v b/src/stat/stat.v index abe71a12..daa4e059 100644 --- a/src/stat/stat.v +++ b/src/stat/stat.v @@ -13,6 +13,11 @@ const app = common.CoreutilInfo{ help: $embed_file('help.txt').to_string() } +// fstypes_data maps the magic numbers the kernel reports for a filesystem to +// its type name. Embedded at module scope: $embed_file inside a function body +// does not resolve on all V versions. +const fstypes_data = $embed_file('fstypes.txt') + // Settings for Utility: stat struct Settings { mut: @@ -66,18 +71,22 @@ struct Statx { stx_dio_offset_align u32 } +// Field types and order mirror the kernel's `struct statvfs`, including the +// trailing spare, because statvfs() hands this struct to libc through a +// voidptr. Every f_* member is `unsigned long`/`fsblkcnt_t`, hence usize. struct Statvfs { - f_bsize u64 // Filesystem block size - f_frsize u64 // Fragment size - f_blocks u64 // Size of fs in f_frsize units - f_bfree u64 // Number of free blocks - f_bavail u64 // Number of free blocks for unprivileged users - f_files u64 // Number of inodes - f_ffree u64 // Number of free inodes - f_favail u64 // Number of free inodes for unprivileged users - f_fsid u64 // Filesystem ID - f_flag u64 // Mount flags - f_namemax u64 // Maximum filename length + f_bsize usize // Filesystem block size + f_frsize usize // Fragment size + f_blocks usize // Size of fs in f_frsize units + f_bfree usize // Number of free blocks + f_bavail usize // Number of free blocks for unprivileged users + f_files usize // Number of inodes + f_ffree usize // Number of free inodes + f_favail usize // Number of free inodes for unprivileged users + f_fsid usize // Filesystem ID + f_flag usize // Mount flags + f_namemax usize // Maximum filename length + f_spare [6]i32 // __f_spare in the C struct; V rejects leading underscores } enum CacheMode { @@ -551,8 +560,7 @@ fn get_mount_list() []MountInfo { } fn get_fs_list() map[string]u32 { - embedded_file := $embed_file('fstypes.txt') - s := embedded_file.to_string() + s := fstypes_data.to_string() assert s[s.len - 1] == `\n`, 'fstypes.txt must be newline-terminated.' mut fslist := map[string]u32{} for i := 0; i < s.len; { diff --git a/src/tail/parse_args.v b/src/tail/parse_args.v index 31f6e8ca..858f67ff 100644 --- a/src/tail/parse_args.v +++ b/src/tail/parse_args.v @@ -34,14 +34,14 @@ fn parse_args(args []string) Args { bytes_arg := fp.string('bytes', `c`, '-1', 'output the last NUM bytes; or use -c + to output ${wrap}' + - 'starting with byte of each file') + 'starting with byte of each file') follow_arg := fp.bool('follow', `f`, false, 'output appended data as the file grows') f_arg := fp.bool('', `F`, false, 'same as --follow=name --retry') lines_arg := fp.string('lines', `n`, '10', 'output the last NUM lines, instead of the last 10; or us${wrap}' + - '-n +NUM to skip NUM-1 lines at the start') + '-n +NUM to skip NUM-1 lines at the start') pid_arg := fp.string('pid', ` `, '', 'with -f, terminate after process ID, PID dies') quiet_arg := fp.bool('quiet', `q`, false, 'never output headers giving file names') @@ -72,9 +72,9 @@ fn parse_args(args []string) Args { files := scan_files_arg(files_arg) return Args{ - bytes: string_to_i64(bytes_arg) or { exit_error(err.msg()) } + bytes: string_to_i64(bytes_arg) or { exit_error(invalid_number('bytes', bytes_arg)) } follow: follow_arg || f_arg - lines: string_to_i64(lines_arg) or { exit_error(err.msg()) } + lines: string_to_i64(lines_arg) or { exit_error(invalid_number('lines', lines_arg)) } pid: pid_arg quiet: quiet_arg || silent_arg retry: f_arg || retry_arg @@ -86,6 +86,12 @@ fn parse_args(args []string) Args { } } +// invalid_number matches the diagnostic GNU tail prints for a +// -c/-n operand it cannot parse. +fn invalid_number(kind string, value string) string { + return 'invalid number of ${kind}: ‘${value}’' +} + fn scan_files_arg(files_arg []string) []string { mut files := []string{} diff --git a/src/tail/tail.v b/src/tail/tail.v index 78d6dbde..7f01d1fb 100644 --- a/src/tail/tail.v +++ b/src/tail/tail.v @@ -1,7 +1,7 @@ // tail - output the last part of files import os import time -import v.mathutil +import math const app_name = 'tail' @@ -82,7 +82,7 @@ fn tail_bytes(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { pos := if args.from_start { args.bytes } else { - mathutil.max(u64(0), stat_size - u64(args.bytes)) + math.max(u64(0), stat_size - u64(args.bytes)) } mut f := os.open(file.name) or { if args.retry { @@ -112,7 +112,7 @@ fn tail_file(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { mut pos := i64(0) loop1: for pos <= end { - len := mathutil.min(end - pos, buf_size) + len := math.min(end - pos, buf_size) f.read_bytes_into(u64(pos), mut buf) or { exit_error(err.msg()) } for i := 0; i < len; i += 1 { @@ -133,8 +133,8 @@ fn tail_file(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { mut pos := end loop2: for pos > 0 { - len := mathutil.min(pos, buf_size) - pos = mathutil.max(pos - buf_size, 0) + len := math.min(pos, buf_size) + pos = math.max(pos - buf_size, 0) f.read_bytes_into(u64(pos), mut buf) or { exit_error(err.msg()) } for i := len - 1; i >= 0; i -= 1 { diff --git a/src/truncate/truncate.v b/src/truncate/truncate.v index 1402f829..a0b918a5 100644 --- a/src/truncate/truncate.v +++ b/src/truncate/truncate.v @@ -202,8 +202,13 @@ fn truncate(settings Settings) { f.close() } if os.exists(fname) { - block_size := if settings.io_blocks { get_block_size(fname) or { - default_block_size} } else { 1 } + block_size := if settings.io_blocks { + get_block_size(fname) or { + default_block_size + } + } else { + 1 + } size := calc_target_size(get_size(settings.reference), settings.size_opt, block_size) os.truncate(fname, size) or { app.quit(message: err.msg()) } @@ -216,8 +221,13 @@ fn truncate(settings Settings) { // If --no-create is set, nothing is done but no error is generated // This is behavior from the original GNU coreutil. if os.exists(fname) { - block_size := if settings.io_blocks { get_block_size(fname) or { - default_block_size} } else { 1 } + block_size := if settings.io_blocks { + get_block_size(fname) or { + default_block_size + } + } else { + 1 + } size := calc_target_size(get_size(fname), settings.size_opt, block_size) os.truncate(fname, size) or { app.quit(message: err.msg()) } } diff --git a/src/unlink/unlink.c.v b/src/unlink/unlink.c.v index 6a00b43e..e450f253 100644 --- a/src/unlink/unlink.c.v +++ b/src/unlink/unlink.c.v @@ -1,6 +1,7 @@ import os #include + $if !windows { #include } diff --git a/src/users/users.c.v b/src/users/users.c.v index b7e3b2dc..9fb26cc4 100644 --- a/src/users/users.c.v +++ b/src/users/users.c.v @@ -1,23 +1,19 @@ import common -pub struct C.utmpx { - ut_type i16 // Type of login. - ut_pid int // Process ID of login process. - ut_line &char // Devicename. - ut_id &char // Inittab ID. - ut_user &char // Username. -} +// `struct utmpx` is picked up from via common/readutmp_nix.c.v. A +// hand written copy here listed only the fields it needed and dropped the +// fixed size char arrays, so its element size did not match the C struct. fn utmp_users(filename &char) []string { mut utmp_buf := []C.utmpx{} - mut users := []string{} + mut names := []string{} common.read_utmp(filename, mut utmp_buf, .user_process) unsafe { for u in utmp_buf { - users << cstring_to_vstring(u.ut_user) + names << cstring_to_vstring(&u.ut_user[0]) } } // Obtain sorted order as GNU coreutils - users.sort() - return users + names.sort() + return names } diff --git a/src/users/users.v b/src/users/users.v index 121fa227..3a89d358 100644 --- a/src/users/users.v +++ b/src/users/users.v @@ -13,9 +13,9 @@ mut: } fn users(settings Settings) { - users := utmp_users(settings.input_file).join(' ') - print(users) - if users != '' { + names := utmp_users(settings.input_file).join(' ') + print(names) + if names != '' { print(common.eol()) } } diff --git a/src/wc/wc.v b/src/wc/wc.v index e928a36d..ba8ff5ff 100644 --- a/src/wc/wc.v +++ b/src/wc/wc.v @@ -59,7 +59,7 @@ fn get_count(chunk FileChunk, last_line_length u32) (Count, u32) { } line_length = 0 } - space, carriage_return, vertical_tab, form_feed { + space, vertical_tab, form_feed { prev_char_is_space = true line_length++ } @@ -96,14 +96,19 @@ mut: mutex sync.Mutex } -fn (mut file_reader FileReader) read_chunk(mut buffer []u8) ?FileChunk { +// read_chunk reads the next buffer_size bytes. os.File.read signals +// exhaustion with an os.Eof error, which the caller has to tell apart from a +// genuine read failure. +fn (mut file_reader FileReader) read_chunk(mut buffer []u8) !FileChunk { file_reader.mutex.@lock() defer { file_reader.mutex.unlock() } - nbytes := - file_reader.file.read(mut buffer) or { return none } // Propagate error. Either EOF or read error. + nbytes := file_reader.file.read(mut buffer) or { return err } + if nbytes == 0 { + return os.Eof{} + } mut chunk := FileChunk{file_reader.last_char_is_space, buffer[..nbytes].clone(), false} file_reader.last_char_is_space = is_space(buffer[nbytes - 1]) if nbytes < buffer.len { @@ -121,16 +126,14 @@ fn file_reader_counter(mut file_reader FileReader) Count { for { chunk := file_reader.read_chunk(mut buffer) or { match err { - none { - // EOF 'error', just break out of the loop. + os.Eof { break } else { - println(err) + eprintln('${application_name}: ${err}') + exit(1) } } - - exit(1) } count, line_length = get_count(chunk, line_length) @@ -256,7 +259,8 @@ fn main() { max_line_length_len := max_line_length.str().len mut col_size := int(0) - if byte(bytes_opt) + byte(chars_opt) + byte(lines_opt) + byte(words_opt) + byte(maxline_opt) == 1 { + // A single selected count is printed without a header or column padding. + if [bytes_opt, chars_opt, lines_opt, words_opt, maxline_opt].filter(it).len == 1 { col_size = 0 } else { if total_line_count_len > col_size { From a62b2cc0ce86c35332212a52d0021bc2b621ce9b Mon Sep 17 00:00:00 2001 From: metif12 Date: Fri, 2 Oct 2026 10:25:32 +0330 Subject: [PATCH 2/2] fix: repair the 10 test files that were failing for unrelated reasons Follow-up to #195, which got the build and the formatting gate green. This brings `make test` from 37/47 to 46/47. The only remaining failure is shuf_test.v, which is issue #189 and gets its own PR. ## common/pwd: group ids came back as garbage getgrouplist() and getgroups() fill an array of gid_t, which is 32 bits wide, but the V declarations handed them a []int, i.e. i64. The kernel wrote the low half of each slot and left the upper half holding the previous entry, so gids arrived as 103079215108 (24 * 2^32 + 4, i.e. adm's 4 with cdrom's 24 above it). The two functions now fill a []u32. getgrgid() was also declared as returning `&C.passwd` rather than `&C.group`. That happened to work because gr_name sits at the same offset as pw_name, but gr_mem/gr_gid do not line up, so there is now a proper struct. groups and id now agree with GNU for the plain, named-user, -nG and -G forms. ## tail: KiB was decimal, and PB/EB used the binary multiplier The suffix table mapped both `kb` and `kib` to 1000, both `mb` and `mib` to 1000^2, and so on, so `tail -c 1KiB` read a third of what it should have. `petabyte` was also `terra * kilo` and `exabyte` `peta * kilo`, i.e. the binary multipliers, instead of continuing the decimal chain. The two chains are now separate and correct, and the test asserts GNU's actual values rather than mirroring the constants. The overflow check was `result == 0 && number != 0`, which cannot catch a wrapped product; it now checks that the division comes back out. `20E` is rejected like GNU rejects it. Z/Y/R/Q stay unsupported, because although GNU's help mentions them, GNU itself rejects all of them with "Value too large for defined data type" (checked against 9.4). Verified end to end against GNU for 2b, k, K, KB, KiB, M, MB, MiB, G, GB, GiB, T, TB, TiB, P, PB, PiB, E, EB and EiB, plus the rejection cases. ## numfmt: 2000.0 and 2e+06 f64.str() always keeps a fractional part and switches to scientific notation above a million, so `--to=none 2000000` printed `2e+06` and `2K` printed `2000.0`. Both now go through a helper based on strlong() that drops a trailing `.0`. ## stat: the default Device: field The default format used `%Dh/%dd`, which printed the device number in hex over the same number in decimal: `830h/2096d` where GNU prints `8,48`. GNU's default is `Device: %Hd,%Ld`, and %H/%L are two character tokens that scan_for_tokens() did not handle; there was already a TODO about it. Both tokens are implemented and the default format matches. ## Three tests were asserting the wrong thing, or crashing * `tail_test` and `fmt_test` captured `&result` for a local in `setup()` and returned the closure. Once setup() returned, the pointer dangled and the closure appended to freed stack memory: `malloc_uninit(0xd0a07408218d1b1f < 0)`. The array header is now heap allocated. (cut_test and paste_test use the same shape but call the closure in scope, so they are fine.) * `string_to_i64_test` used `!` on an option return, which does not compile. * `sum_test` had `const util` next to `import io.util`. The import is aliased. * `truncate_test` computed its expectations as `24 * 1000 * 1000 * 1000`. V's `int` is 32-bit, so the expectation wrapped to -1769803776 while the value under test, a u64, was right all along. The expected values are now u64. ## Testing `v run build.vsh` builds all 76 utilities, `v fmt -verify .` passes, and `v test .` reports 46 of 47 files passing, up from 37. --- common/pwd/pwd_nix.c.v | 37 ++++++++++++------ src/fmt/fmt_test.v | 14 ++++--- src/numfmt/numfmt.v | 16 +++++++- src/stat/stat.v | 24 +++++++++--- src/sum/sum_test.v | 6 ++- src/tail/string_to_i64.v | 36 +++++++++++------ src/tail/string_to_i64_test.v | 74 ++++++++++++++++++++--------------- src/tail/tail_test.v | 14 ++++--- src/truncate/truncate_test.v | 6 +-- 9 files changed, 146 insertions(+), 81 deletions(-) diff --git a/common/pwd/pwd_nix.c.v b/common/pwd/pwd_nix.c.v index aa30f817..e32721fe 100644 --- a/common/pwd/pwd_nix.c.v +++ b/common/pwd/pwd_nix.c.v @@ -22,11 +22,21 @@ struct C.passwd { pw_shell &char } +// getgrgid returns a `struct group`, not a `struct passwd`. Reading it through +// a passwd only worked because gr_name happens to sit at the same offset. +struct C.group { + gr_name &char + gr_passwd &char + gr_gid u32 + gr_mem voidptr // char ** +} + fn C.getpwuid(int) &C.passwd -fn C.getgrgid(int) &C.passwd +fn C.getgrgid(int) &C.group fn C.getpwnam(&char) &C.passwd -fn C.getgroups(int, &int) int -fn C.getgrouplist(&char, int, &int, &int) int +// gid_t is a 32 bit type, so both of these fill a u32 array. +fn C.getgroups(int, &u32) int +fn C.getgrouplist(&char, u32, &u32, &int) int pub fn get_uid_for_name(username string) !int { r := C.getpwnam(username.str) @@ -60,7 +70,7 @@ pub fn get_name_for_gid(gid int) !string { } return os.error_posix() } - return cstring_to_vstring(r.pw_name) + return cstring_to_vstring(r.gr_name) } } @@ -82,26 +92,29 @@ pub fn get_groups(username string) ![]int { user := get_userinfo_for_name(username)! unsafe { mut count := init_group_buf_size - mut groups := []int{len: count} - mut res := C.getgrouplist(username.str, user.gid, &groups[0], &count) + // getgrouplist stores gid_t values, which are 32 bits wide. Handing it + // a []int would leave the upper half of every slot holding whatever was + // there before, so the ids come back as things like 103079215108. + mut gids := []u32{len: count} + mut res := C.getgrouplist(username.str, u32(user.gid), &gids[0], &count) // If the buffer was not big enough, count will be updated with the // number we need. if res == -1 { - groups = []int{len: count} - res = C.getgrouplist(username.str, user.gid, &groups[0], &count) + gids = []u32{len: count} + res = C.getgrouplist(username.str, u32(user.gid), &gids[0], &count) } - return groups[0..count] + return gids[0..count].map(int(it)) } } fn get_effective_groups_up_to(limit int) !(int, []int) { - mut groups := []int{len: init_group_buf_size} unsafe { - num_groups := C.getgroups(limit, &groups[0]) + gids := []u32{len: init_group_buf_size} + num_groups := C.getgroups(limit, &gids[0]) if num_groups < 0 { return os.error_posix() } - return num_groups, groups + return num_groups, gids[0..num_groups].map(int(it)) } } diff --git a/src/fmt/fmt_test.v b/src/fmt/fmt_test.v index dfe90e51..13ae6c05 100644 --- a/src/fmt/fmt_test.v +++ b/src/fmt/fmt_test.v @@ -24,13 +24,15 @@ fn to_tmp_file(data []string) string { } fn setup() (fn (s string), fn () []string) { - mut result := []string{} - mut result_ref := &result - out_fn := fn [mut result_ref] (s string) { - result_ref << s + // The array header has to be heap allocated: a pointer to a local would + // dangle as soon as setup() returned, and the closures would then append to + // freed stack memory. + mut result := &[]string{} + out_fn := fn [mut result] (s string) { + (*result) << s } - result_fn := fn [mut result_ref] () []string { - return *result_ref + result_fn := fn [result] () []string { + return *result } return out_fn, result_fn } diff --git a/src/numfmt/numfmt.v b/src/numfmt/numfmt.v index 2a106662..f1a6efa5 100644 --- a/src/numfmt/numfmt.v +++ b/src/numfmt/numfmt.v @@ -63,7 +63,7 @@ fn numfmt(number string, mut app App, options Options) !string { mut result := match true { options.pformat.len != 0 { unsafe { strconv.v_sprintf(options.pformat, n) } } options.grouping { commaize(n) } - options.to == 'none' { n.str() } + options.to == 'none' { number_str(n) } else { num_to_str(n, options) or { handle_error(err.msg(), mut app, options) } } } @@ -170,7 +170,19 @@ fn readable_size(size f64, unit Unit, rounding string) !string { } sz /= kb } - return size.str() + return number_str(size) +} + +// number_str renders n the way GNU numfmt does: plain decimal digits, never +// exponent notation, and no trailing ".0" on an integral value. f64.str() +// switches to scientific notation above a million and always keeps the +// fractional part, which would turn 2000000 into "2e+06" and 2000 into "2000.0". +fn number_str(n f64) string { + s := n.strlong() + if s.ends_with('.0') { + return s[..s.len - 2] + } + return s } fn scale_number(num f64, pow int, options Options) i64 { diff --git a/src/stat/stat.v b/src/stat/stat.v index daa4e059..320069af 100644 --- a/src/stat/stat.v +++ b/src/stat/stat.v @@ -100,6 +100,10 @@ enum CacheMode { const tokens_statx = 'aAbBCdDfFgGhimnNosrRtTuUwWxXyYzZV'.bytes() const tokens_statvfs = 'abcdfilnsStT'.bytes() +// The two character format tokens, which need to be matched before the single +// character ones. +const two_char_tokens = ['Hd', 'Ld'] + @[inline] fn make_dev(major u32, minor u32) u32 { return major << 8 | minor @@ -170,6 +174,14 @@ fn process_token(token string, st Statx, path string, mtab []MountInfo) string { 'D' { '${make_dev(st.stx_dev_major, st.stx_dev_minor):x}' } + 'Hd' { + // major device number, decimal; GNU's default Device: field + '${st.stx_dev_major}' + } + 'Ld' { + // minor device number, decimal; GNU's default Device: field + '${st.stx_dev_minor}' + } 'f' { '${st.stx_mode:4x}' } @@ -339,11 +351,13 @@ fn scan_num(s &string) int { return (*s).len } -// TODO: When we upgrade to coreutil 9.4 compatibility, we will need two-byte tokens -// if (*s).len > 1 && (*s)[..2] in ['Hd', 'Hr', 'Ld', 'Lr'] { -// return 2 -// } +// Some of GNU's format tokens are two characters wide, e.g. %Hd and %Ld for +// the major and minor device numbers. They have to be recognised before the +// single character tokens, otherwise %Hd would be read as %H followed by "d". fn scan_for_tokens(s &string, tokens []u8) int { + if (*s).len > 1 && (*s)[..2] in two_char_tokens { + return 2 + } if (*s).len > 0 { if (*s)[0] in tokens { return 1 @@ -507,7 +521,7 @@ fn args() Settings { if st.file_system { st.format = ' File: "%n"\n ID: %-8i Namelen: %-7l Type: %T\nBlock size: %-10s Fundamental block size: %S\nBlocks: Total: %-10b Free: %-10f Available: %a\nInodes: Total: %-10c Free: %d\n' } else { - st.format = ' File: %V\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\nDevice: %Dh/%dd\tInode: %-10i Links: %h\nAccess: (%04a/%10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\nAccess: %x\nModify: %y\nChange: %z\n Birth: %w\n' + st.format = ' File: %V\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\nDevice: %Hd,%Ld\tInode: %-10i Links: %h\nAccess: (%04a/%10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\nAccess: %x\nModify: %y\nChange: %z\n Birth: %w\n' } } } else { diff --git a/src/sum/sum_test.v b/src/sum/sum_test.v index 21168625..ba5fe000 100644 --- a/src/sum/sum_test.v +++ b/src/sum/sum_test.v @@ -1,5 +1,7 @@ import os -import io.util +// Aliased: the module is called `util`, which would clash with the `util` +// constant below holding this utility's name. +import io.util as io_util import common.testing const eol = testing.output_eol() @@ -97,7 +99,7 @@ fn test_sysv_several_files_succeeds() { } fn sum_arbitrary_value(value string, arg string) !os.Result { - mut f, path := util.temp_file()! + mut f, path := io_util.temp_file()! f.write_string('${value}\n')! f.close() res := $if windows { diff --git a/src/tail/string_to_i64.v b/src/tail/string_to_i64.v index b44ce979..d90e109b 100644 --- a/src/tail/string_to_i64.v +++ b/src/tail/string_to_i64.v @@ -1,32 +1,35 @@ // convert strings like 10K to i164 const block = i64(512) +// binary (IEC) multipliers, i.e. GNU's suffix without the extra i // **1 const kilo = i64(1024) -const kilobyte = i64(1000) // **2 const mega = kilo * kilo -const megabyte = kilobyte * kilobyte // **3 const giga = mega * kilo -const gigabyte = megabyte * kilobyte // **4 const terra = giga * kilo -const terrabyte = gigabyte * kilobyte // **5 const peta = terra * kilo -const petabyte = terra * kilobyte // **6 const exa = peta * kilo -const exabyte = peta * kilo // **7 const zetta = exa * kilo + +// decimal (SI) multipliers +const kilobyte = i64(1000) +const megabyte = kilobyte * kilobyte +const gigabyte = megabyte * kilobyte +const terrabyte = gigabyte * kilobyte +const petabyte = terrabyte * kilobyte +const exabyte = petabyte * kilobyte const zettabyte = exabyte * kilobyte fn string_to_i64(s string) ?i64 { @@ -52,17 +55,23 @@ fn string_to_i64(s string) ?i64 { multiplier := match suffix.to_lower() { 'b' { block } 'k' { kilo } - 'kb', 'kib' { kilobyte } + 'kb' { kilobyte } + 'kib' { kilo } 'm' { mega } - 'mb', 'mib' { megabyte } + 'mb' { megabyte } + 'mib' { mega } 'g' { giga } - 'gb', 'gib' { gigabyte } + 'gb' { gigabyte } + 'gib' { giga } 't' { terra } - 'tb', 'tib' { terrabyte } + 'tb' { terrabyte } + 'tib' { terra } 'p' { peta } - 'pb', 'pib' { petabyte } + 'pb' { petabyte } + 'pib' { peta } 'e' { exa } - 'eb', 'eib' { exabyte } + 'eb' { exabyte } + 'eib' { exa } // oddball formats found in __xstrtol source 'c' { 1 } 'w' { 2 } @@ -70,7 +79,8 @@ fn string_to_i64(s string) ?i64 { } result := number * multiplier - if result == 0 && number != 0 { + if number != 0 && result / number != multiplier { + // the multiplication wrapped around return none } return result diff --git a/src/tail/string_to_i64_test.v b/src/tail/string_to_i64_test.v index 5d1eeaae..6c56f063 100644 --- a/src/tail/string_to_i64_test.v +++ b/src/tail/string_to_i64_test.v @@ -1,38 +1,48 @@ module main -fn test_string_to_i64_conversions() { - assert string_to_i64('1')! == 1 - assert string_to_i64('+1')! == 1 - assert string_to_i64('-1')! == -1 - - assert string_to_i64('2b')! == 2 * block - - assert string_to_i64('11k')! == 11 * kilo - assert string_to_i64('12K')! == 12 * kilo - assert string_to_i64('13KB')! == 13 * kilobyte - assert string_to_i64('14KiB')! == 14 * kilobyte - - assert string_to_i64('15M')! == 15 * mega - assert string_to_i64('16MB')! == 16 * megabyte - assert string_to_i64('17mib')! == 17 * megabyte - - assert string_to_i64('18T')! == 18 * terra - assert string_to_i64('18TB')! == 18 * terrabyte - assert string_to_i64('18TiB')! == 18 * terrabyte - - assert string_to_i64('10P')! == 10 * peta - assert string_to_i64('10PB')! == 10 * petabyte - assert string_to_i64('10PiB')! == 10 * petabyte - - assert string_to_i64('5E')! == 5 * exa - assert string_to_i64('5EB')! == 5 * exabyte - assert string_to_i64('5EiB')! == 5 * exabyte - - overflow_r := string_to_i64('5R') or { -1 } - assert overflow_r == -1 +// string_to_i64 returns an option, so it has to be unwrapped explicitly; `!` +// does not work on an option. +fn to_i64(s string) i64 { + return string_to_i64(s) or { panic('string_to_i64(' + s + ') returned none') } +} - overflow_q := string_to_i64('5Q') or { -1 } - assert overflow_q == -1 +fn test_string_to_i64_conversions() { + assert to_i64('1') == 1 + assert to_i64('+1') == 1 + assert to_i64('-1') == -1 + + assert to_i64('2b') == 2 * block + + assert to_i64('11k') == 11 * kilo + assert to_i64('12K') == 12 * kilo + assert to_i64('13KB') == 13 * kilobyte + assert to_i64('14KiB') == 14 * kilo + + assert to_i64('15M') == 15 * mega + assert to_i64('16MB') == 16 * megabyte + assert to_i64('17mib') == 17 * mega + + assert to_i64('18T') == 18 * terra + assert to_i64('18TB') == 18 * terrabyte + assert to_i64('18TiB') == 18 * terra + + assert to_i64('10P') == 10 * peta + assert to_i64('10PB') == 10 * petabyte + assert to_i64('10PiB') == 10 * peta + + assert to_i64('5E') == 5 * exa + assert to_i64('5EB') == 5 * exabyte + assert to_i64('5EiB') == 5 * exa + + // GNU tail's help mentions Z, Y, R and Q, but it rejects all of them + // with "Value too large for defined data type" + for big in ['1Z', '1ZB', '1ZiB', '1Y', '1YB', '1R', '1RB', '1Q'] { + assert string_to_i64(big) == none + } + + // an out of range multiple must be rejected rather than wrap around + overflow_e := string_to_i64('20E') or { -1 } + assert overflow_e == -1 // invalid checks t0 := string_to_i64('') or { -1 } diff --git a/src/tail/tail_test.v b/src/tail/tail_test.v index 64082246..4b39cf02 100644 --- a/src/tail/tail_test.v +++ b/src/tail/tail_test.v @@ -5,13 +5,15 @@ import os fn setup() (fn (s string), fn () string) { os.chdir(os.dir(@FILE)) or { exit_error(err.msg()) } - mut result := []string{} - mut result_ref := &result - out_fn := fn [mut result_ref] (s string) { - result_ref << s + // The array header has to be heap allocated: a pointer to a local would + // dangle as soon as setup() returned, and the closures would then append to + // freed stack memory. + mut result := &[]string{} + out_fn := fn [mut result] (s string) { + (*result) << s } - result_fn := fn [mut result_ref] () string { - return result_ref.join('') + result_fn := fn [result] () string { + return result.join('') } return out_fn, result_fn } diff --git a/src/truncate/truncate_test.v b/src/truncate/truncate_test.v index 67010835..da10714b 100644 --- a/src/truncate/truncate_test.v +++ b/src/truncate/truncate_test.v @@ -28,7 +28,7 @@ fn test_size_parser() { s = parse_size_opt('-24GB') assert s.mode == .subtract - assert s.size == 24 * 1000 * 1000 * 1000 + assert s.size == u64(24) * 1000 * 1000 * 1000 s = parse_size_opt('<13kB') assert s.mode == .at_most @@ -46,12 +46,12 @@ fn test_size_parser() { s = parse_size_opt('/14PB') assert s.mode == .round_down - assert s.size == 14 * 1000 * 1000 * 1000 * 1000 * 1000 + assert s.size == u64(14) * 1000 * 1000 * 1000 * 1000 * 1000 assert s != t s = parse_size_opt('%1E') assert s.mode == .round_up - assert s.size == 1 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert s.size == u64(1024) * 1024 * 1024 * 1024 * 1024 * 1024 // TODO: Test u64 overflows }