summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 18:25:55 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 18:25:55 +0200
commit1ea2f68e6a9af135e6cb3274fdf28d4ddc5812d5 (patch)
treef28246586e387293d0c8beaf6b445210aaab8c48
parentklog/tty/pager: no more truncated lines — full lines flow, terminal wraps (diff)
downloadrs-dos-1ea2f68e6a9af135e6cb3274fdf28d4ddc5812d5.tar.gz
rs-dos-1ea2f68e6a9af135e6cb3274fdf28d4ddc5812d5.zip
fix pi-lens warnings: unwraps, shell SC2046, dup code (fmtbuf/scripts/hexdump/cat/ahci/pci)
- memory.rs: self-test Layout::from_size_align no longer panics — the test fails cleanly on an invalid layout - fat.rs: v() no longer unwraps the active-volume slot — invariant violation logs + halts instead of panicking mid-disk-io - strbuf: shared FmtBuf replaces the duplicated PanicBuf/FmtBuf (main.rs + sched.rs) - test-disk.sh: SC2046 — conditional --fresh arg no longer relies on word splitting - scripts: boot.sh/boot-image.sh share lib-boot-args.sh (dup args loop) - shell: cat/hexdump stdin pairs unified with Option<&str> marker; hexdump format_row folded into format_partial - ahci: read/write_sectors unified behind dma_sectors - pci: config_read/write share config_addr
-rw-r--r--rsdos/src/main.rs21
-rw-r--r--rsnix/src/ahci.rs18
-rw-r--r--rsnix/src/fat.rs12
-rw-r--r--rsnix/src/memory.rs12
-rw-r--r--rsnix/src/pci.rs28
-rw-r--r--rsnix/src/sched.rs18
-rw-r--r--rsnix/src/strbuf.rs28
-rwxr-xr-xscripts/boot-image.sh25
-rwxr-xr-xscripts/boot.sh22
-rw-r--r--scripts/lib-boot-args.sh38
-rwxr-xr-xscripts/test-disk.sh6
-rw-r--r--shell/src/lib.rs94
12 files changed, 152 insertions, 170 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index 7ca031c..700dcc7 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -9,20 +9,9 @@ use rsnix::uefi::*;
9// Panic handler 9// Panic handler
10// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
11 11
12/// Small fmt::Write over a byte slice (no allocation, bounded). 12// ---------------------------------------------------------------------------
13struct PanicBuf<'a> { 13// Panic handler
14 b: &'a mut [u8], 14// ---------------------------------------------------------------------------
15 n: usize,
16}
17
18impl core::fmt::Write for PanicBuf<'_> {
19 fn write_str(&mut self, s: &str) -> core::fmt::Result {
20 let m = s.len().min(self.b.len() - self.n);
21 self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]);
22 self.n += m;
23 Ok(())
24 }
25}
26 15
27#[panic_handler] 16#[panic_handler]
28fn panic(info: &PanicInfo) -> ! { 17fn panic(info: &PanicInfo) -> ! {
@@ -35,12 +24,12 @@ fn panic(info: &PanicInfo) -> ! {
35 if rsnix::sched::is_ready() && !rsnix::sched::in_isr() && rsnix::sched::current() >= 2 { 24 if rsnix::sched::is_ready() && !rsnix::sched::in_isr() && rsnix::sched::current() >= 2 {
36 let mut buf = [0u8; 192]; 25 let mut buf = [0u8; 192];
37 let n = { 26 let n = {
38 let mut w = PanicBuf { b: &mut buf, n: 0 }; 27 let mut w = rsnix::strbuf::FmtBuf::new(&mut buf);
39 let _ = write!(&mut w, "{}", info.message()); 28 let _ = write!(&mut w, "{}", info.message());
40 if let Some(loc) = info.location() { 29 if let Some(loc) = info.location() {
41 let _ = write!(&mut w, " at {}:{}", loc.file(), loc.line()); 30 let _ = write!(&mut w, " at {}:{}", loc.file(), loc.line());
42 } 31 }
43 w.n 32 w.len()
44 }; 33 };
45 let msg = core::str::from_utf8(&buf[..n]).unwrap_or("task panic"); 34 let msg = core::str::from_utf8(&buf[..n]).unwrap_or("task panic");
46 rsnix::sched::abort_current(msg); 35 rsnix::sched::abort_current(msg);
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs
index 72da07d..a77b341 100644
--- a/rsnix/src/ahci.rs
+++ b/rsnix/src/ahci.rs
@@ -510,22 +510,24 @@ fn active() -> Option<&'static Disk> {
510 disks().get(unsafe { ACTIVE }).and_then(|d| d.as_ref()) 510 disks().get(unsafe { ACTIVE }).and_then(|d| d.as_ref())
511} 511}
512 512
513/// DMA `count` sectors starting at `lba` into the selected disk's buffer. 513/// Bounds-checked DMA of `count` sectors starting at `lba` into/out of the
514pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { 514/// selected disk's buffer (single code path for both directions).
515fn dma_sectors(lba: u64, count: u32, cmd: u8, write: bool) -> Result<(), ()> {
515 let d = active().ok_or(())?; 516 let d = active().ok_or(())?;
516 if lba + count as u64 > d.sectors { 517 if lba + count as u64 > d.sectors {
517 return Err(()); 518 return Err(());
518 } 519 }
519 issue(d, ATA_READ_DMA_EXT, lba, count, false) 520 issue(d, cmd, lba, count, write)
521}
522
523/// DMA `count` sectors starting at `lba` into the selected disk's buffer.
524pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> {
525 dma_sectors(lba, count, ATA_READ_DMA_EXT, false)
520} 526}
521 527
522/// DMA `count` sectors starting at `lba` out of the selected disk's buffer. 528/// DMA `count` sectors starting at `lba` out of the selected disk's buffer.
523pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { 529pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> {
524 let d = active().ok_or(())?; 530 dma_sectors(lba, count, ATA_WRITE_DMA_EXT, true)
525 if lba + count as u64 > d.sectors {
526 return Err(());
527 }
528 issue(d, ATA_WRITE_DMA_EXT, lba, count, true)
529} 531}
530 532
531/// Pointer to the selected disk's 4 KiB DMA buffer. 533/// Pointer to the selected disk's 4 KiB DMA buffer.
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
index bad01cd..eda81ed 100644
--- a/rsnix/src/fat.rs
+++ b/rsnix/src/fat.rs
@@ -167,8 +167,18 @@ fn write_sector(lba: u64, data: &[u8; SECTOR]) -> Result<(), ()> {
167 167
168// --- cluster math ---------------------------------------------------------- 168// --- cluster math ----------------------------------------------------------
169 169
170/// Current volume's metadata — only ever reached with a selected volume
171/// (the fs layer calls `select_vol` first), so a missing slot is a kernel
172/// invariant violation, not a recoverable error.
170fn v() -> &'static mut Volume { 173fn v() -> &'static mut Volume {
171 vols().get_mut(*active_vol()).unwrap() 174 let i = *active_vol();
175 match vols().get_mut(i) {
176 Some(v) => v,
177 None => {
178 crate::serial_println!("fat: internal error — no volume at slot {}", i);
179 crate::interrupts::halt();
180 }
181 }
172} 182}
173 183
174fn spc() -> usize { 184fn spc() -> usize {
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs
index 7c3eda0..e03af8b 100644
--- a/rsnix/src/memory.rs
+++ b/rsnix/src/memory.rs
@@ -458,7 +458,9 @@ pub fn self_test() -> bool {
458 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); 458 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
459 let align = 16usize << (seed as usize % 8); 459 let align = 16usize << (seed as usize % 8);
460 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); 460 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
461 let layout = Layout::from_size_align(size, align).unwrap(); 461 let Ok(layout) = Layout::from_size_align(size, align) else {
462 return false; // impossible: size <= 2048, align <= 2048
463 };
462 let p = ALLOCATOR.alloc(layout); 464 let p = ALLOCATOR.alloc(layout);
463 if p.is_null() { 465 if p.is_null() {
464 break; // heap exhausted — stop 466 break; // heap exhausted — stop
@@ -493,7 +495,9 @@ pub fn self_test() -> bool {
493 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); 495 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
494 let align = 16usize << (seed as usize % 8); 496 let align = 16usize << (seed as usize % 8);
495 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); 497 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
496 let layout = Layout::from_size_align(size, align).unwrap(); 498 let Ok(layout) = Layout::from_size_align(size, align) else {
499 return false; // impossible: size <= 2048, align <= 2048
500 };
497 let p = ALLOCATOR.alloc(layout); 501 let p = ALLOCATOR.alloc(layout);
498 if p.is_null() { 502 if p.is_null() {
499 return false; // failed to reuse freed space 503 return false; // failed to reuse freed space
@@ -510,7 +514,9 @@ pub fn self_test() -> bool {
510 if *ptr == 0 { 514 if *ptr == 0 {
511 continue; 515 continue;
512 } 516 }
513 let layout = Layout::from_size_align(*size, *align).unwrap(); 517 let Ok(layout) = Layout::from_size_align(*size, *align) else {
518 return false; // impossible: sizes/aligns came from the loop above
519 };
514 ALLOCATOR.dealloc(*ptr as *mut u8, layout); 520 ALLOCATOR.dealloc(*ptr as *mut u8, layout);
515 } 521 }
516 let stats = heap_stats(); 522 let stats = heap_stats();
diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs
index 21d7536..b9c2265 100644
--- a/rsnix/src/pci.rs
+++ b/rsnix/src/pci.rs
@@ -46,25 +46,25 @@ fn inl(port: u16) -> u32 {
46 val 46 val
47} 47}
48 48
49/// Read a dword from a device's config space (`reg` must be dword-aligned). 49/// Encode the PCI config-space port address for (bus, dev, fn, reg);
50pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { 50/// `reg` must be dword-aligned.
51 let addr = 0x8000_0000 51fn config_addr(bus: u8, device: u8, func: u8, reg: u8) -> u32 {
52 0x8000_0000
52 | ((bus as u32) << 16) 53 | ((bus as u32) << 16)
53 | ((device as u32) << 11) 54 | ((device as u32) << 11)
54 | ((func as u32) << 8) 55 | ((func as u32) << 8)
55 | ((reg as u32) & 0xFC); 56 | ((reg as u32) & 0xFC)
56 outl(CONFIG_ADDRESS, addr); 57}
58
59/// Read a dword from a device's config space (`reg` must be dword-aligned).
60pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 {
61 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg));
57 inl(CONFIG_DATA) 62 inl(CONFIG_DATA)
58} 63}
59 64
60/// Write a dword to a device's config space (`reg` must be dword-aligned). 65/// Write a dword to a device's config space (`reg` must be dword-aligned).
61pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { 66pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) {
62 let addr = 0x8000_0000 67 outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg));
63 | ((bus as u32) << 16)
64 | ((device as u32) << 11)
65 | ((func as u32) << 8)
66 | ((reg as u32) & 0xFC);
67 outl(CONFIG_ADDRESS, addr);
68 outl(CONFIG_DATA, val) 68 outl(CONFIG_DATA, val)
69} 69}
70 70
@@ -83,7 +83,11 @@ pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option<u64> {
83 0 83 0
84 }; 84 };
85 let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0); 85 let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0);
86 if base == 0 { None } else { Some(base) } 86 if base == 0 {
87 None
88 } else {
89 Some(base)
90 }
87} 91}
88 92
89/// Enumerate every device/function present on all buses. 93/// Enumerate every device/function present on all buses.
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs
index 4b10503..6ffa745 100644
--- a/rsnix/src/sched.rs
+++ b/rsnix/src/sched.rs
@@ -372,7 +372,7 @@ pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize {
372 let rip = unsafe { (frame_ptr as *const IntFrame).read().rip }; 372 let rip = unsafe { (frame_ptr as *const IntFrame).read().rip };
373 let mut buf = [0u8; 96]; 373 let mut buf = [0u8; 96];
374 let n = { 374 let n = {
375 let mut f = FmtBuf { b: &mut buf, n: 0 }; 375 let mut f = crate::strbuf::FmtBuf::new(&mut buf);
376 let _ = fmt::write( 376 let _ = fmt::write(
377 &mut f, 377 &mut f,
378 core::format_args!( 378 core::format_args!(
@@ -386,7 +386,7 @@ pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize {
386 rip 386 rip
387 ), 387 ),
388 ); 388 );
389 f.n 389 f.len()
390 }; 390 };
391 abort_current(core::str::from_utf8(&buf[..n]).unwrap_or("task fault")) 391 abort_current(core::str::from_utf8(&buf[..n]).unwrap_or("task fault"))
392} 392}
@@ -528,17 +528,3 @@ fn idle_main() {
528 } 528 }
529 } 529 }
530} 530}
531
532struct FmtBuf<'a> {
533 b: &'a mut [u8],
534 n: usize,
535}
536
537impl fmt::Write for FmtBuf<'_> {
538 fn write_str(&mut self, s: &str) -> fmt::Result {
539 let m = s.len().min(self.b.len() - self.n);
540 self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]);
541 self.n += m;
542 Ok(())
543 }
544}
diff --git a/rsnix/src/strbuf.rs b/rsnix/src/strbuf.rs
index 262720d..0e3ea56 100644
--- a/rsnix/src/strbuf.rs
+++ b/rsnix/src/strbuf.rs
@@ -69,3 +69,31 @@ impl<const N: usize> fmt::Debug for String<N> {
69 write!(f, "{:?}", self.as_str()) 69 write!(f, "{:?}", self.as_str())
70 } 70 }
71} 71}
72
73/// Small `fmt::Write` over a byte slice — no allocation, bounded.
74/// Shared by the panic handler (task-panic message) and the scheduler
75/// (page-fault abort message) so they don't each reinvent it.
76pub struct FmtBuf<'a> {
77 b: &'a mut [u8],
78 n: usize,
79}
80
81impl<'a> FmtBuf<'a> {
82 pub fn new(b: &'a mut [u8]) -> Self {
83 FmtBuf { b, n: 0 }
84 }
85
86 /// Bytes written so far (clamped at the buffer length).
87 pub fn len(&self) -> usize {
88 self.n
89 }
90}
91
92impl fmt::Write for FmtBuf<'_> {
93 fn write_str(&mut self, s: &str) -> fmt::Result {
94 let m = s.len().min(self.b.len() - self.n);
95 self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]);
96 self.n += m;
97 Ok(())
98 }
99}
diff --git a/scripts/boot-image.sh b/scripts/boot-image.sh
index 913e5cb..e8c093c 100755
--- a/scripts/boot-image.sh
+++ b/scripts/boot-image.sh
@@ -34,29 +34,8 @@ GUI=""
34DEBUG="" 34DEBUG=""
35 35
36# ---- args ---- 36# ---- args ----
37while [ $# -gt 0 ]; do 37source "$SCRIPT_DIR/lib-boot-args.sh"
38 case "$1" in 38parse_boot_args "$@"
39 --gui | -g) GUI="-vga std" ;;
40 --debug | -d) DEBUG="-s -S" ;;
41 --mem)
42 shift
43 MEM="$1"
44 ;;
45 --smp)
46 shift
47 SMP="$1"
48 ;;
49 --help | -h)
50 head -20 "$0" | grep "^#" | sed 's/^# //'
51 exit 0
52 ;;
53 *)
54 echo "unknown: $1"
55 exit 1
56 ;;
57 esac
58 shift
59done
60 39
61# ---- build image if missing ---- 40# ---- build image if missing ----
62if [ ! -f "$IMAGE" ]; then 41if [ ! -f "$IMAGE" ]; then
diff --git a/scripts/boot.sh b/scripts/boot.sh
index 99fd103..5b8e6fa 100755
--- a/scripts/boot.sh
+++ b/scripts/boot.sh
@@ -45,33 +45,21 @@ GUI=""
45DEBUG="" 45DEBUG=""
46 46
47# ---- args ---- 47# ---- args ----
48while [ $# -gt 0 ]; do 48source "$SCRIPT_DIR/lib-boot-args.sh"
49# boot.sh-specific flags live here; everything else is shared.
50handle_arg() {
49 case "$1" in 51 case "$1" in
50 --gui | -g) GUI="-vga std" ;;
51 --debug | -d) DEBUG="-s -S" ;;
52 --release | -r) 52 --release | -r)
53 PROFILE="release" 53 PROFILE="release"
54 CARGO_FLAGS="--release" 54 CARGO_FLAGS="--release"
55 ;; 55 ;;
56 --mem)
57 shift
58 MEM="$1"
59 ;;
60 --smp)
61 shift
62 SMP="$1"
63 ;;
64 --help | -h)
65 head -20 "$0" | grep "^#" | sed 's/^# //'
66 exit 0
67 ;;
68 *) 56 *)
69 echo "unknown: $1" 57 echo "unknown: $1"
70 exit 1 58 exit 1
71 ;; 59 ;;
72 esac 60 esac
73 shift 61}
74done 62parse_boot_args "$@"
75 63
76OUT_DIR="target/x86_64-unknown-uefi/$PROFILE" 64OUT_DIR="target/x86_64-unknown-uefi/$PROFILE"
77KERNEL="$OUT_DIR/rsdos.efi" 65KERNEL="$OUT_DIR/rsdos.efi"
diff --git a/scripts/lib-boot-args.sh b/scripts/lib-boot-args.sh
new file mode 100644
index 0000000..ec97500
--- /dev/null
+++ b/scripts/lib-boot-args.sh
@@ -0,0 +1,38 @@
1#!/usr/bin/env bash
2#
3# scripts/lib-boot-args.sh — shared CLI parsing for the QEMU boot scripts.
4# Sourced, not executed. The caller sets default values for GUI/DEBUG/
5# MEM/SMP first, then calls `parse_boot_args "$@"`. A caller with extra
6# flags defines its own `handle_arg()` to intercept them before the
7# shared cases run (unknown args still fail loudly).
8set -euo pipefail
9
10parse_boot_args() {
11 while [ $# -gt 0 ]; do
12 case "$1" in
13 --gui | -g) GUI="-vga std" ;;
14 --debug | -d) DEBUG="-s -S" ;;
15 --mem)
16 shift
17 MEM="$1"
18 ;;
19 --smp)
20 shift
21 SMP="$1"
22 ;;
23 --help | -h)
24 head -20 "$0" | grep "^#" | sed 's/^# //'
25 exit 0
26 ;;
27 *) handle_arg "$1" ;;
28 esac
29 shift
30 done
31}
32
33# Default: no extra flags. A script with more defines its own before
34# calling parse_boot_args.
35handle_arg() {
36 echo "unknown: $1"
37 exit 1
38}
diff --git a/scripts/test-disk.sh b/scripts/test-disk.sh
index 11a9b05..0268fff 100755
--- a/scripts/test-disk.sh
+++ b/scripts/test-disk.sh
@@ -72,7 +72,11 @@ cargo build --package rsdos --target x86_64-unknown-uefi
72echo "==> 2/6 deploy to ESP + ensure disk image" 72echo "==> 2/6 deploy to ESP + ensure disk image"
73mkdir -p "$ESP_DIR/EFI/BOOT" 73mkdir -p "$ESP_DIR/EFI/BOOT"
74cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" 74cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI"
75"$SCRIPT_DIR/mkdisk.sh" $([ "$FRESH" -eq 1 ] && echo --fresh) "$IMG" 75if [ "$FRESH" -eq 1 ]; then
76 "$SCRIPT_DIR/mkdisk.sh" --fresh "$IMG"
77else
78 "$SCRIPT_DIR/mkdisk.sh" "$IMG"
79fi
76 80
77echo "==> 3/6 boot QEMU (headless)" 81echo "==> 3/6 boot QEMU (headless)"
78kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true 82kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index 617e5a4..4640697 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -515,7 +515,12 @@ impl Pipeline {
515 } 515 }
516 } 516 }
517 Filter::Pause => { 517 Filter::Pause => {
518 let StageState::Pause { shown, screen, cols } = st else { 518 let StageState::Pause {
519 shown,
520 screen,
521 cols,
522 } = st
523 else {
519 unreachable!("stage state does not match filter") 524 unreachable!("stage state does not match filter")
520 }; 525 };
521 for l in cur.drain(..) { 526 for l in cur.drain(..) {
@@ -871,7 +876,7 @@ fn cmd_cat(p: &Parsed) {
871 if p.args.is_empty() { 876 if p.args.is_empty() {
872 // Path-less `cat` with no input keyword reads the keyboard 877 // Path-less `cat` with no input keyword reads the keyboard
873 // until ^D. 878 // until ^D.
874 cat_stdin(&mut pipe); 879 cat_stdin(&mut pipe, None);
875 } else { 880 } else {
876 for path in &p.args { 881 for path in &p.args {
877 if !cat_file(&mut pipe, path) { 882 if !cat_file(&mut pipe, path) {
@@ -885,7 +890,7 @@ fn cmd_cat(p: &Parsed) {
885 } 890 }
886 Input::Til(marker) => { 891 Input::Til(marker) => {
887 if p.args.is_empty() { 892 if p.args.is_empty() {
888 cat_stdin_until(&mut pipe, marker); 893 cat_stdin(&mut pipe, Some(marker));
889 } else { 894 } else {
890 // `cat <path> TIL <marker>` — the paths are the input 895 // `cat <path> TIL <marker>` — the paths are the input
891 // source, truncated at the first marker line. 896 // source, truncated at the first marker line.
@@ -904,34 +909,21 @@ fn cmd_cat(p: &Parsed) {
904 pipe.end(); 909 pipe.end();
905} 910}
906 911
907fn cat_stdin(pipe: &mut Pipeline) -> bool { 912/// Read lines from the tty until ^C/^D (or an exact marker line, when
913/// `marker` is Some — the marker itself is not part of the input).
914fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool {
908 loop { 915 loop {
909 match tty::read_line() { 916 match tty::read_line() {
910 LineInput::Line(l) => { 917 LineInput::Line(l) => {
911 if !pipe.line(l.as_str()) { 918 if marker.is_some() && l.as_str() == marker.unwrap() {
912 return false;
913 }
914 }
915 LineInput::Interrupted => return false, // ^C aborts the stream
916 LineInput::Eof => return true, // ^D = EOF
917 }
918 }
919}
920
921fn cat_stdin_until(pipe: &mut Pipeline, marker: &str) -> bool {
922 loop {
923 match tty::read_line() {
924 LineInput::Line(l) => {
925 // Marker line is not part of the input.
926 if l.as_str() == marker {
927 return true; 919 return true;
928 } 920 }
929 if !pipe.line(l.as_str()) { 921 if !pipe.line(l.as_str()) {
930 return false; 922 return false;
931 } 923 }
932 } 924 }
933 LineInput::Interrupted => return false, 925 LineInput::Interrupted => return false, // ^C aborts the stream
934 LineInput::Eof => return true, 926 LineInput::Eof => return true, // ^D = EOF
935 } 927 }
936 } 928 }
937} 929}
@@ -1030,25 +1022,6 @@ struct HexCtx<'a> {
1030 alive: bool, 1022 alive: bool,
1031} 1023}
1032 1024
1033fn format_row(addr: u64, row: &[u8; 16]) -> String {
1034 let mut out = String::new();
1035 use core::fmt::Write as _;
1036 let _ = core::write!(out, "{:08x} ", addr);
1037 for &b in row {
1038 let _ = core::write!(out, "{:02x} ", b);
1039 }
1040 out.push_str(" |");
1041 for &b in row {
1042 out.push(if b.is_ascii_graphic() || b == b' ' {
1043 b as char
1044 } else {
1045 '.'
1046 });
1047 }
1048 out.push('|');
1049 out
1050}
1051
1052fn format_partial(addr: u64, row: &[u8]) -> String { 1025fn format_partial(addr: u64, row: &[u8]) -> String {
1053 let mut out = String::new(); 1026 let mut out = String::new();
1054 use core::fmt::Write as _; 1027 use core::fmt::Write as _;
@@ -1081,7 +1054,7 @@ impl HexCtx<'_> {
1081 self.row[self.row_len] = b; 1054 self.row[self.row_len] = b;
1082 self.row_len += 1; 1055 self.row_len += 1;
1083 if self.row_len == 16 { 1056 if self.row_len == 16 {
1084 let line = format_row(self.pos, &self.row); 1057 let line = format_partial(self.pos, &self.row);
1085 self.alive = self.pipe.line(&line); 1058 self.alive = self.pipe.line(&line);
1086 self.pos += 16; 1059 self.pos += 16;
1087 self.row_len = 0; 1060 self.row_len = 0;
@@ -1106,7 +1079,7 @@ fn cmd_hexdump(p: &Parsed) {
1106 match &p.input { 1079 match &p.input {
1107 Input::None => { 1080 Input::None => {
1108 if p.args.is_empty() { 1081 if p.args.is_empty() {
1109 hexdump_stdin(&mut pipe); 1082 hexdump_stdin(&mut pipe, None);
1110 } else { 1083 } else {
1111 for path in &p.args { 1084 for path in &p.args {
1112 if !hexdump_file(&mut pipe, path) { 1085 if !hexdump_file(&mut pipe, path) {
@@ -1120,7 +1093,7 @@ fn cmd_hexdump(p: &Parsed) {
1120 } 1093 }
1121 Input::Til(marker) => { 1094 Input::Til(marker) => {
1122 if p.args.is_empty() { 1095 if p.args.is_empty() {
1123 hexdump_stdin_until(&mut pipe, marker); 1096 hexdump_stdin(&mut pipe, Some(marker));
1124 } else { 1097 } else {
1125 // `hexdump <path> TIL <marker>` — the paths are the input 1098 // `hexdump <path> TIL <marker>` — the paths are the input
1126 // source, truncated at the first marker line. 1099 // source, truncated at the first marker line.
@@ -1176,34 +1149,9 @@ fn hexdump_file(pipe: &mut Pipeline, path: &str) -> bool {
1176 ctx.alive 1149 ctx.alive
1177} 1150}
1178 1151
1179fn hexdump_stdin(pipe: &mut Pipeline) { 1152/// Hex-dump lines from the tty until ^C/^D (or an exact marker line,
1180 let mut ctx = HexCtx { 1153/// when `marker` is Some — the marker itself is not dumped).
1181 pipe, 1154fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) {
1182 pos: 0,
1183 row: [0u8; 16],
1184 row_len: 0,
1185 alive: true,
1186 };
1187 loop {
1188 match tty::read_line() {
1189 LineInput::Line(l) => {
1190 ctx.feed(l.as_str().as_bytes());
1191 if !ctx.alive {
1192 break;
1193 }
1194 ctx.feed(b"\n");
1195 if !ctx.alive {
1196 break;
1197 }
1198 }
1199 LineInput::Interrupted => break,
1200 LineInput::Eof => break,
1201 }
1202 }
1203 ctx.flush();
1204}
1205
1206fn hexdump_stdin_until(pipe: &mut Pipeline, marker: &str) {
1207 let mut ctx = HexCtx { 1155 let mut ctx = HexCtx {
1208 pipe, 1156 pipe,
1209 pos: 0, 1157 pos: 0,
@@ -1214,7 +1162,7 @@ fn hexdump_stdin_until(pipe: &mut Pipeline, marker: &str) {
1214 loop { 1162 loop {
1215 match tty::read_line() { 1163 match tty::read_line() {
1216 LineInput::Line(l) => { 1164 LineInput::Line(l) => {
1217 if l.as_str() == marker { 1165 if marker.is_some() && l.as_str() == marker.unwrap() {
1218 break; 1166 break;
1219 } 1167 }
1220 ctx.feed(l.as_str().as_bytes()); 1168 ctx.feed(l.as_str().as_bytes());