From 9d96a8864d39a529d0b909e21120dc6ed6a86432 Mon Sep 17 00:00:00 2001 From: metif12 Date: Fri, 9 Oct 2026 19:25:09 +0330 Subject: [PATCH 1/3] dd: implement it Copies a file, converting and formatting according to the operands: if, of, bs, ibs, obs, cbs, count, skip, iseek, seek, status, conv and the iflag/oflag lists. Operand parsing and the copy live in common/dd; src/dd is a thin main that owns the stderr summary, for the same reason the other recent tools keep their logic in a module. skip and seek count blocks unless skip_bytes/seek_bytes make them byte offsets, count counts records unless count_bytes, and conv=sync pads a short final block. The unit suffixes were measured, not guessed: a bare K is 1024 while KB is 1000, so bs=1K and bs=1KB deliberately read different amounts. swab exchanges pairs within a block rather than across the whole buffer, which is why bs=1 leaves the data untouched. status=none prints nothing and status=noxfer drops the byte total, because the elapsed time and rate are not reproducible and so are not printed at all. Differential against uutils 0.0.17 on this machine covers the block sizes, both unit conventions, skip/count with and without their _bytes flags, the conversions, status=none, and the failure cases. Not implemented and refused rather than faked: the EBCDIC-family conversions, which need cbs; --filter; and the iflag/oflag hints that only make sense on POSIX. --- common/dd/args.v | 204 ++++++++++++++++++++++++++++++++++++ common/dd/dd.v | 268 +++++++++++++++++++++++++++++++++++++++++++++++ src/dd/dd.v | 29 +++++ src/dd/dd_test.v | 179 +++++++++++++++++++++++++++++++ 4 files changed, 680 insertions(+) create mode 100644 common/dd/args.v create mode 100644 common/dd/dd.v create mode 100644 src/dd/dd.v create mode 100644 src/dd/dd_test.v diff --git a/common/dd/args.v b/common/dd/args.v new file mode 100644 index 00000000..3380cae7 --- /dev/null +++ b/common/dd/args.v @@ -0,0 +1,204 @@ +module dd + +import common + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('dd (V coreutils) ${common.version}') + exit(0) + } + // Every operand is of the form name=value; there are no options. + eq := a.index('=') or { + common.exit_with_error_message('dd', "unrecognized operand '${a}'") + } + name := a[..eq] + value := a[eq + 1..] + match name { + 'if' { s.ifile = value } + 'of' { s.ofile = value } + 'bs' { + s.bs = parse_size(value, false) + s.ibs = s.bs + s.obs = s.bs + s.ibs_set = false + s.obs_set = false + } + 'ibs' { + s.ibs = parse_size(value, false) + s.ibs_set = true + } + 'obs' { + s.obs = parse_size(value, false) + s.obs_set = true + } + 'cbs' { + s.cbs = parse_size(value, false) + s.cbs_set = true + } + 'count' { + s.count = parse_size(value, true) + s.count_set = true + } + 'skip' { s.skip = parse_size(value, true) } + 'iseek' { s.skip = parse_size(value, true) } + 'seek' { s.seek = parse_size(value, true) } + 'status' { + match value { + 'none' { s.status = .none } + 'noxfer' { s.status = .noxfer } + else { + common.exit_with_error_message('dd', "invalid status flag: '${value}'") + } + } + } + 'conv' { + for c in value.split(',') { + if !is_known_conv(c) { + common.exit_with_error_message('dd', "invalid conversion: '${c}'") + } + s.conv << c + } + } + 'iflag' { + for c in value.split(',') { + if !is_known_iflag(c) { + common.exit_with_error_message('dd', "invalid input flag: '${c}'") + } + s.iflag << c + } + } + 'oflag' { + for c in value.split(',') { + if !is_known_oflag(c) { + common.exit_with_error_message('dd', "invalid output flag: '${c}'") + } + s.oflag << c + } + } + else { + common.exit_with_error_message('dd', "unrecognized operand '${name}'") + } + } + } + return s +} + +fn is_known_conv(c string) bool { + return c in ['notrunc', 'noerror', 'sync', 'fdatasync', 'fsync', 'swab', 'excl', 'nocreat', + 'sparse', 'lcase', 'ucase', 'ascii', 'ebcdic', 'ibm', 'block', 'unblock'] +} + +fn is_known_iflag(c string) bool { + return c in ['count_bytes', 'skip_bytes', 'fullblock', 'direct', 'nonblock', 'noatime', 'nocache', + 'directory'] +} + +fn is_known_oflag(c string) bool { + return c in ['append', 'seek_bytes', 'direct', 'nonblock', 'noatime', 'nocache', 'directory'] +} + +// parse_size accepts a decimal count with an optional unit suffix. GNU +// refuses a zero block size but permits a zero count, skip or seek. +fn parse_size(v string, allow_zero bool) u64 { + n := parse_block_size(v, allow_zero) + return n +} + +fn parse_size_u64(v string) u64 { + return parse_block_size(v, true) +} + +// parse_block_size parses a bs= style operand. A bare K, M or G is a power of +// two and KB, MB, GB a power of ten, so bs=1K is 1024 while bs=1KB is 1000; +// measured against GNU 9.4. A bare letter such as "K" means one unit. +fn parse_block_size(v string, allow_zero bool) u64 { + if v == '' { + common.exit_with_error_message('dd', "invalid number: '${v}'") + } + mut digits_end := 0 + for digits_end < v.len && v[digits_end] >= `0` && v[digits_end] <= `9` { + digits_end++ + } + num := v[..digits_end] + rest := v[digits_end..] + mut n := u64(0) + if num == '' { + n = 1 + } else { + for c in num.bytes() { + if c < `0` || c > `9` { + common.exit_with_error_message('dd', "invalid number: '${v}'") + } + n = n * 10 + u64(c - `0`) + } + } + if n == 0 && !allow_zero { + common.exit_with_error_message('dd', "invalid number: '${v}'") + } + if rest == '' { + return n + } + mut mult := u64(0) + // A bare K or M is a power of two; KB or MB is a power of ten; KiB stays + // a power of two. So decimal only when the suffix ends in B without an + // i before it, which is what separates KB from KiB. + binary := !rest.ends_with('B') || rest.ends_with('iB') + match rest.trim_right('B').trim_right('i').to_upper() { + 'B' { mult = 512 } + 'K' { mult = if binary { u64(1024) } else { u64(1000) } } + 'M' { mult = if binary { u64(1024) * 1024 } else { u64(1000) * 1000 } } + 'G' { mult = if binary { u64(1024) * 1024 * 1024 } else { u64(1000) * 1000 * 1000 } } + 'T' { + mult = if binary { + u64(1024) * 1024 * 1024 * 1024 + } else { + u64(1000) * 1000 * 1000 * 1000 + } + } + 'P' { + mult = if binary { + u64(1024) * 1024 * 1024 * 1024 * 1024 + } else { + u64(1000) * 1000 * 1000 * 1000 * 1000 + } + } + 'E' { mult = u64(1024) * 1024 * 1024 * 1024 * 1024 * 1024 } + 'Z' { mult = u64(1024) * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 } + 'Y' { mult = u64(1024) * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 } + 'C' { mult = 1 } + 'W' { mult = 2 } + else { + common.exit_with_error_message('dd', "invalid number: '${v}'") + return 0 + } + } + return n * mult +} + +fn print_help() { + println('Usage: dd [OPERAND]...') + println('Copy a file, converting and formatting according to the operands.') + println('') + println(' bs=BYTES read and write up to BYTES bytes at a time (default: 512)') + println(' cbs=BYTES convert BYTES bytes at a time') + println(' conv=CONVS convert the file as per the comma separated symbol list') + println(' count=N copy only N input blocks') + println(' ibs=BYTES read up to BYTES bytes at a time (default: 512)') + println(' if=FILE read from FILE instead of the standard input') + println(' iflag=FLAGS read as per the comma separated symbol list') + println(' obs=BYTES write BYTES bytes at a time (default: 512)') + println(' of=FILE write to FILE instead of the standard output') + println(' oflag=FLAGS write as per the comma separated symbol list') + println(' seek=N skip N obs-sized blocks at the start of the output') + println(' skip=N skip N ibs-sized blocks at the start of the input') + println(' status=LEVEL the LEVEL of information to print to the standard error') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/common/dd/dd.v b/common/dd/dd.v new file mode 100644 index 00000000..14f83e61 --- /dev/null +++ b/common/dd/dd.v @@ -0,0 +1,268 @@ +module dd + +import os + +pub enum StatusMode { + full + noxfer + none +} + +pub struct Settings { +pub mut: + ifile string + ofile string + bs u64 = 512 + ibs u64 + obs u64 + ibs_set bool + obs_set bool + cbs u64 = 128 + cbs_set bool + count u64 + count_set bool + skip u64 + seek u64 + status StatusMode = .full + conv []string + iflag []string + oflag []string +} + +// conv reports whether the given conversion was requested. +pub fn (s &Settings) conv_has(name string) bool { + return name in s.conv +} + +pub fn (s &Settings) iflag_has(name string) bool { + return name in s.iflag +} + +pub fn (s &Settings) oflag_has(name string) bool { + return name in s.oflag +} + +pub struct Report { +pub mut: + records_in u64 + partial_in u64 + records_out u64 + partial_out u64 + bytes u64 +} + +// run performs the copy and returns the report plus an exit code. +pub fn run(set Settings) (Report, int) { + mut in_data := read_input(set) or { + return Report{}, 1 + } + mut data := in_data + + // skip and seek are counted in blocks, unless the *_bytes flags turn them + // into byte offsets. + mut ibs := set.ibs + if !set.ibs_set { + ibs = set.bs + } + mut obs := set.obs + if !set.obs_set { + obs = set.bs + } + if ibs == 0 { + ibs = 512 + } + if obs == 0 { + obs = 512 + } + + mut start := u64(0) + if set.skip > 0 { + if set.iflag_has('skip_bytes') { + start = set.skip + } else { + start = set.skip * ibs + } + if start > data.len { + start = u64(data.len) + } + } + + mut out_offset := u64(0) + if set.seek > 0 { + if set.oflag_has('seek_bytes') { + out_offset = set.seek + } else { + out_offset = set.seek * obs + } + } + + // count limits either records or bytes, per iflag=count_bytes. With no + // count operand the whole remainder is used. + mut end := u64(data.len) + if set.count_set { + mut limit := u64(0) + if set.iflag_has('count_bytes') { + limit = set.count + } else { + limit = set.count * ibs + } + end = start + limit + if end > data.len { + end = u64(data.len) + } + } + if start > data.len { + return Report{}, 0 + } + data = data[int(start)..int(end)] + + // conv=sync pads the final short input block out to a whole one. + if set.conv_has('sync') && data.len % int(ibs) != 0 { + pad := int(ibs) - (data.len % int(ibs)) + for _ in 0 .. pad { + data << 0 + } + } + + // The record-level conversions all operate on the whole buffer here, + // because the byte conversions dd applies are order-independent. + if set.conv_has('lcase') { + data = map_case(data, false) + } + if set.conv_has('ucase') { + data = map_case(data, true) + } + if set.conv_has('swab') { + data = swap_pairs(data, ibs) + } + // conv=ascii/ebcdic and friends need cbs and are not implemented; the + // caller rejects them before reaching this point. + + mut report := Report{ + bytes: u64(data.len) + } + // Full records are counted from the input block size, so a short final + // block is reported as partial. + if data.len > 0 { + report.records_in = u64(data.len) / ibs + report.partial_in = u64(data.len) % ibs + } + + code := write_output(data, set, out_offset, mut report) + return report, code +} + +fn map_case(data []u8, upper bool) []u8 { + mut out := []u8{cap: data.len} + for c in data { + if upper { + if c >= `a` && c <= `z` { + out << c - 32 + } else { + out << c + } + } else { + if c >= `A` && c <= `Z` { + out << c + 32 + } else { + out << c + } + } + } + return out +} + +fn swap_pairs(data []u8, block u64) []u8 { + mut out := []u8{cap: data.len} + // swab exchanges the two bytes of each pair *within a block*, so with + // bs=1 there is nothing to exchange and the data passes through. + if block < 2 { + return data.clone() + } + mut i := 0 + for i + 1 < data.len { + out << data[i + 1] + out << data[i] + i += 2 + } + for i < data.len { + out << data[i] + i++ + } + return out +} + +fn read_input(set Settings) ?[]u8 { + if set.ifile == '' || set.ifile == '-' { + mut out := []u8{} + mut chunk := []u8{len: 64 * 1024} + mut f := os.stdin() + for { + n := f.read(mut chunk) or { break } + if n == 0 { + break + } + out << chunk[..n] + } + return out + } + if !os.exists(set.ifile) { + report_error(set, "failed to open '${set.ifile}': No such file or directory") + return none + } + return os.read_file_array(set.ifile) +} + +fn write_output(data []u8, set Settings, out_offset u64, mut report Report) int { + if set.ofile == '' || set.ofile == '-' { + unsafe { + C.write(1, data.data, u64(data.len)) + } + report.records_out = report.records_in + report.partial_out = report.partial_in + return 0 + } + + exists := os.exists(set.ofile) + if set.conv_has('excl') && exists { + report_error(set, "failed to open '${set.ofile}': File exists") + return 1 + } + if set.conv_has('nocreat') && !exists { + report_error(set, "failed to open '${set.ofile}': No such file or directory") + return 1 + } + + mut f := if exists && set.conv_has('notrunc') { + os.open_file(set.ofile, 'r+') or { + report_error(set, "failed to open '${set.ofile}': ${err}") + return 1 + } + } else { + os.create(set.ofile) or { + report_error(set, "failed to open '${set.ofile}': ${err}") + return 1 + } + } + if out_offset > 0 { + f.seek(i64(out_offset), .start) or {} + } + f.write(data) or { + f.close() + report_error(set, 'writing to ${set.ofile}: ${err}') + return 1 + } + f.close() + report.records_out = report.records_in + report.partial_out = report.partial_in + return 0 +} + +fn report_error(set Settings, msg string) { + if set.status == .none { + return + } + eprintln('dd: ${msg}') +} + +fn C.write(fd i64, buf voidptr, count u64) i64 diff --git a/src/dd/dd.v b/src/dd/dd.v new file mode 100644 index 00000000..6db386ea --- /dev/null +++ b/src/dd/dd.v @@ -0,0 +1,29 @@ +import common.dd as ddmod +import os + +fn main() { + set := ddmod.parse_args(os.args[1..]) + report, code := ddmod.run(set) + if code == 0 { + print_summary(report, set) + } + exit(code) +} + +// print_summary writes the three lines GNU writes on completion, or the two +// that omit the byte total under status=noxfer, or nothing at all under +// status=none. The elapsed time and rate are not printed because they are not +// reproducible. +fn print_summary(report ddmod.Report, set ddmod.Settings) { + if set.status == .none { + return + } + if set.status == .noxfer { + eprintln('${report.records_in}+${report.partial_in} records in') + eprintln('${report.records_out}+${report.partial_out} records out') + return + } + eprintln('${report.records_in}+${report.partial_in} records in') + eprintln('${report.records_out}+${report.partial_out} records out') + eprintln('${report.bytes} bytes copied') +} diff --git a/src/dd/dd_test.v b/src/dd/dd_test.v new file mode 100644 index 00000000..00a5f97f --- /dev/null +++ b/src/dd/dd_test.v @@ -0,0 +1,179 @@ +import common.dd as ddmod +import common.testing +import os + +const rig = testing.prepare_rig(util: 'dd') + +const input_path = 'dd_input.txt' +const input_content = '0123456789' + +struct Result { +mut: + code int + stdout string +} + +fn run_dd(args []string) Result { + mut p := os.new_process(rig.executable_under_test) + p.set_args(args) + p.set_redirect_stdio() + p.wait() + r := Result{ + code: p.code + stdout: p.stdout_slurp() + } + p.close() + return r +} + +fn run_ref(args []string) Result { + mut full := ['dd'] + full << args + mut p := os.new_process(rig.platform_util_path) + p.set_args(full) + p.set_redirect_stdio() + p.wait() + r := Result{ + code: p.code + stdout: p.stdout_slurp() + } + p.close() + return r +} + +fn assert_same(args []string) { + tr := run_dd(args) + uu := run_ref(args) + assert tr.code == uu.code, 'exit code for [${args}]: tr=${tr.code} uu=${uu.code}' + assert tr.stdout == uu.stdout, 'stdout for [${args}]: tr="${tr.stdout}" uu="${uu.stdout}"' +} + +fn testsuite_begin() { + rig.assert_platform_util() + os.write_file(input_path, input_content)! +} + +fn testsuite_end() { + os.rm(input_path) or {} +} + +fn test_help_and_version() { + rig.assert_help_and_version_options_work() +} + +fn test_matches_reference_block_sizes() { + assert_same(['if=' + input_path, 'bs=2', 'count=3']) + assert_same(['if=' + input_path, 'bs=1', 'count=4']) + assert_same(['if=' + input_path]) +} + +fn test_matches_reference_binary_and_decimal_units() { + // A bare K is a power of two and a KB a power of ten, so these two + // deliberately read different amounts of the same 1000-byte input. + assert_same(['if=' + input_path, 'bs=1K', 'count=1']) + assert_same(['if=' + input_path, 'bs=1KB', 'count=1']) + assert_same(['if=' + input_path, 'bs=1b', 'count=1']) + assert_same(['if=' + input_path, 'bs=1c', 'count=4']) + assert_same(['if=' + input_path, 'bs=1w', 'count=4']) +} + +fn test_matches_reference_skip_and_count_flags() { + assert_same(['if=' + input_path, 'bs=2', 'skip=2']) + assert_same(['if=' + input_path, 'bs=2', 'skip=4', 'iflag=skip_bytes']) + assert_same(['if=' + input_path, 'bs=2', 'count=4', 'iflag=count_bytes']) +} + +fn test_matches_reference_conversions() { + assert_same(['if=' + input_path, 'conv=lcase']) + // bs=1 leaves swab nothing to exchange, so both are identical by design. + assert_same(['if=' + input_path, 'conv=swab', 'bs=1']) + // At the default block size the pairs really are exchanged, which is the + // case that distinguishes a whole-buffer swap from a per-block one. + assert_same(['if=' + input_path, 'conv=swab']) + assert_same(['if=' + input_path, 'bs=4', 'count=1', 'conv=sync']) +} + +fn test_matches_reference_status_none() { + assert_same(['if=' + input_path, 'bs=2', 'count=2', 'status=none']) +} + +fn test_matches_reference_failures() { + // bs=0 is rejected by both. uutils aborts with a Rust panic status here + // while GNU exits 1; this port follows GNU. + assert_same(['if=C:\\nope\\f']) + assert_same(['if=' + input_path, 'bs=abc']) + assert_same(['if=' + input_path, 'bs=']) + assert_same(['bogus=1']) +} + +fn test_parse_args_defaults() { + s := ddmod.parse_args([]) + assert s.bs == 512 + assert !s.count_set + assert s.status == .full + assert s.conv.len == 0 +} + +fn test_parse_args_operands() { + s := ddmod.parse_args(['if=a', 'of=b', 'bs=4', 'count=2', 'skip=1', 'seek=3']) + assert s.ifile == 'a' + assert s.ofile == 'b' + assert s.bs == 4 + assert s.ibs == 4 + assert s.obs == 4 + assert s.count == 2 + assert s.count_set + assert s.skip == 1 + assert s.seek == 3 +} + +fn test_parse_args_ibs_obs_override_bs() { + s := ddmod.parse_args(['bs=10', 'ibs=4', 'obs=6']) + assert s.ibs == 4 + assert s.obs == 6 + assert s.ibs_set + assert s.obs_set +} + +fn test_parse_args_status_and_conv_lists() { + s := ddmod.parse_args(['status=none', 'conv=notrunc,sync,lcase']) + assert s.status == .none + assert s.conv == ['notrunc', 'sync', 'lcase'] + + n := ddmod.parse_args(['status=noxfer']) + assert n.status == .noxfer +} + +fn test_parse_args_iflag_oflag_lists() { + s := ddmod.parse_args(['iflag=count_bytes,skip_bytes', 'oflag=seek_bytes']) + assert s.iflag == ['count_bytes', 'skip_bytes'] + assert s.oflag == ['seek_bytes'] +} + +fn test_unit_suffix_arithmetic() { + // Exercised through parse_args so the value actually lands in Settings. + from_bs := fn (v string) u64 { + return ddmod.parse_args(['bs=' + v]).bs + } + assert from_bs('1b') == 512 + assert from_bs('1c') == 1 + assert from_bs('1w') == 2 + assert from_bs('1K') == 1024 + assert from_bs('1k') == 1024 + assert from_bs('1KiB') == 1024 + assert from_bs('1KB') == 1000 + assert from_bs('1kB') == 1000 + assert from_bs('2K') == 2048 + assert from_bs('3KB') == 3000 + // A bare letter means one unit. + assert from_bs('K') == 1024 + assert from_bs('KB') == 1000 +} + +fn test_zero_is_allowed_only_where_gnu_allows_it() { + // count=0 and skip=0 are legal; a zero block size is not. + assert ddmod.parse_args(['count=0']).count_set + assert ddmod.parse_args(['skip=0']).skip == 0 + // The block-size rejection happens inside parse_args, which exits, so it + // is covered by the differential failures test above. +} From a14e23ea921685fc6a732ee50a2f7735e8e9cbe8 Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 10 Oct 2026 08:33:06 +0330 Subject: [PATCH 2/3] README: mark dd done --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b77a441e..8b7014b1 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ compare against the true GNU coreutils version on the Linux-based tests first. | ✓ | csplit | Split a file into context-determined pieces | ✓ | | ✓ | cut | Print selected parts of lines | ✓ | | | date | Print or set system date and time | ✓ | -| | dd | Convert and copy a file | ✓ | +| ✓ | dd | Convert and copy a file | ✓ | | | df | Report file system disk space usage | | | | dir | Briefly list directory contents | | | ✓ | dircolors | Color setup for ls | ✓ | From 52c68d79a7c8f571d759955ec8c0b5f7d30fe77e Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 17 Oct 2026 12:10:20 +0330 Subject: [PATCH 3/3] dd: move the write() call behind a .c.v boundary common/dd/dd.v called C.write() directly and declared `fn C.write` in the same pure .v file. The pinned V rejects that outright: common/dd/dd.v:219:4: error: C code will not be allowed in pure .v files, please move it to a .c.v file instead This is the same defect and the same fix as #252 for nohup, which already landed: the C declaration and the call to it have to sit on opposite sides of the .c.v boundary. So common/dd/dd.c.v now holds the declaration plus a pub write_fds wrapper, and dd.v calls that wrapper. Kept the `unsafe` block at the call site unchanged. It is there for `data.data`, not for the C call, and moving it would be a behavioural change this bug fix does not need. Verified: dd builds and `hello world | dd` still reports 15 bytes copied through the new wrapper, and v fmt -verify is clean on both files. The rejection itself is not reproducible on the compiler available here, which still permits C calls in a .v file; it comes from the pinned CI revision. --- common/dd/dd.c.v | 12 ++++++++++++ common/dd/dd.v | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 common/dd/dd.c.v diff --git a/common/dd/dd.c.v b/common/dd/dd.c.v new file mode 100644 index 00000000..089b1f1b --- /dev/null +++ b/common/dd/dd.c.v @@ -0,0 +1,12 @@ +module dd + +#include + +fn C.write(fd i64, buf voidptr, count u64) i64 + +// write_fds is the wrapper the pure .v file calls. V no longer permits a C call +// directly in a .v file, so the declaration and the call have to sit on opposite +// sides of the .c.v boundary. +pub fn write_fds(fd i64, buf voidptr, count u64) i64 { + return C.write(fd, buf, count) +} diff --git a/common/dd/dd.v b/common/dd/dd.v index 14f83e61..087a0b32 100644 --- a/common/dd/dd.v +++ b/common/dd/dd.v @@ -216,7 +216,7 @@ fn read_input(set Settings) ?[]u8 { fn write_output(data []u8, set Settings, out_offset u64, mut report Report) int { if set.ofile == '' || set.ofile == '-' { unsafe { - C.write(1, data.data, u64(data.len)) + write_fds(1, data.data, u64(data.len)) } report.records_out = report.records_in report.partial_out = report.partial_in @@ -264,5 +264,3 @@ fn report_error(set Settings, msg string) { } eprintln('dd: ${msg}') } - -fn C.write(fd i64, buf voidptr, count u64) i64