os_dev_toolkit/fmt.rs
1//! Formatting helpers for diagnostics in `no_std` environments.
2//!
3//! The goal of this module is to provide deterministic, allocation-free formatting helpers that
4//! are frequently useful during OS development:
5//!
6//! - [`crate::fmt::HexSlice`] for stable, compact `Debug` printing of byte slices.
7//! - [`crate::fmt::hexdump_to_sink`] for a classic hex dump layout (offset/hex/ascii).
8//! - [`crate::fmt::ByteFmt`] for human-friendly binary unit formatting.
9//! - [`crate::fmt::Addr`] for consistent pointer/address formatting.
10
11use core::fmt;
12use core::fmt::Write;
13
14use crate::log::LogSink;
15
16/// Wrapper type providing a stable [`Debug`](core::fmt::Debug) representation for `&[u8]`.
17///
18/// This is intentionally compact and deterministic:
19///
20/// ```text
21/// [00 01 02 ff]
22/// ```
23///
24/// This is handy in kernel logs where `{:?}` output should be predictable.
25pub struct HexSlice<'a>(pub &'a [u8]);
26
27impl fmt::Debug for HexSlice<'_> {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "[")?;
30 for (i, b) in self.0.iter().enumerate() {
31 if i != 0 {
32 write!(f, " ")?;
33 }
34 write!(f, "{:02x}", b)?;
35 }
36 write!(f, "]")
37 }
38}
39
40/// Writes a classic hex dump to a [`LogSink`].
41///
42/// Each line is formatted as:
43///
44/// ```text
45/// 0x00000000: 01 02 03 ... |....|
46/// ```
47///
48/// - `bytes`: input data to render.
49/// - `sink`: output destination.
50/// - `columns`: number of bytes per line. If `0`, defaults to `16`.
51///
52/// This function is best-effort: it ignores formatting errors because the underlying sink is
53/// infallible by contract.
54pub fn hexdump_to_sink(bytes: &[u8], sink: &mut dyn LogSink, columns: usize) {
55 let cols = if columns == 0 { 16 } else { columns };
56
57 let mut offset = 0usize;
58 while offset < bytes.len() {
59 let end = core::cmp::min(offset + cols, bytes.len());
60 let chunk = &bytes[offset..end];
61
62 sink.write_str("0x");
63 {
64 struct Adapter<'a>(&'a mut dyn LogSink);
65 impl fmt::Write for Adapter<'_> {
66 fn write_str(&mut self, s: &str) -> fmt::Result {
67 self.0.write_str(s);
68 Ok(())
69 }
70 }
71 let mut a = Adapter(sink);
72 let _ = write!(a, "{:08x}: ", offset);
73 for b in chunk {
74 let _ = write!(a, "{:02x} ", b);
75 }
76
77 if end - offset < cols {
78 for _ in 0..(cols - (end - offset)) {
79 let _ = write!(a, " ");
80 }
81 }
82
83 let _ = write!(a, "|");
84 for &b in chunk {
85 let c = if (0x20..=0x7e).contains(&b) {
86 b as char
87 } else {
88 '.'
89 };
90 let _ = write!(a, "{c}");
91 }
92 if end - offset < cols {
93 for _ in 0..(cols - (end - offset)) {
94 let _ = write!(a, " ");
95 }
96 }
97 let _ = write!(a, "|");
98 }
99 sink.write_str("\n");
100 offset = end;
101 }
102}
103
104/// Formats a byte count using binary units (KiB, MiB, GiB).
105///
106/// The output keeps three decimals for larger units and rounds down (truncation), which is often
107/// preferable in diagnostics.
108///
109/// Examples:
110///
111/// - `ByteFmt(999)` -> `"999 B"`
112/// - `ByteFmt(1024)` -> `"1.000 KiB"`
113/// - `ByteFmt(1024*1024)` -> `"1.000 MiB"`
114pub struct ByteFmt(pub u64);
115
116impl fmt::Display for ByteFmt {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 const KIB: u64 = 1024;
119 const MIB: u64 = 1024 * 1024;
120 const GIB: u64 = 1024 * 1024 * 1024;
121
122 let b = self.0;
123 if b >= GIB {
124 write!(f, "{}.{:03} GiB", b / GIB, (b % GIB) * 1000 / GIB)
125 } else if b >= MIB {
126 write!(f, "{}.{:03} MiB", b / MIB, (b % MIB) * 1000 / MIB)
127 } else if b >= KIB {
128 write!(f, "{}.{:03} KiB", b / KIB, (b % KIB) * 1000 / KIB)
129 } else {
130 write!(f, "{b} B")
131 }
132 }
133}
134
135/// Formats an address as lower-hex with `0x` prefix.
136///
137/// This is mainly a readability wrapper for kernel logs.
138pub struct Addr(pub usize);
139
140impl fmt::LowerHex for Addr {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 write!(f, "0x{:x}", self.0)
143 }
144}