Skip to main content

rns_cli/
format.rs

1//! Formatting utilities matching Python RNS output style.
2
3pub use rns_core::display::{b256_rep, prettyb256rep};
4
5/// Format a byte count as a human-readable string.
6/// Matches Python's `RNS.prettysize()`.
7pub fn size_str(num: u64) -> String {
8    if num < 1000 {
9        return format!("{} B", num);
10    }
11    let units = ["B", "KB", "MB", "GB", "TB", "PB"];
12    let mut val = num as f64;
13    let mut unit_idx = 0;
14    while val >= 1000.0 && unit_idx < units.len() - 1 {
15        val /= 1000.0;
16        unit_idx += 1;
17    }
18    format!("{:.2} {}", val, units[unit_idx])
19}
20
21/// Format a bitrate as a human-readable string.
22/// Matches Python's `RNS.prettyspeed()`.
23pub fn speed_str(bps: u64) -> String {
24    if bps < 1000 {
25        return format!("{} b/s", bps);
26    }
27    let units = ["b/s", "Kb/s", "Mb/s", "Gb/s", "Tb/s"];
28    let mut val = bps as f64;
29    let mut unit_idx = 0;
30    while val >= 1000.0 && unit_idx < units.len() - 1 {
31        val /= 1000.0;
32        unit_idx += 1;
33    }
34    format!("{:.2} {}", val, units[unit_idx])
35}
36
37/// Format a destination hash as a hex string.
38/// Matches Python's `RNS.prettyhexrep()`.
39pub fn prettyhexrep(hash: &[u8]) -> String {
40    hash.iter()
41        .map(|b| format!("{:02x}", b))
42        .collect::<Vec<_>>()
43        .join("")
44}
45
46/// Format a duration in seconds as a human-readable string.
47/// Matches Python's `RNS.prettytime()`.
48pub fn prettytime(secs: f64) -> String {
49    if secs < 0.0 {
50        return "now".into();
51    }
52    let total_secs = secs as u64;
53    if total_secs == 0 {
54        return "now".into();
55    }
56
57    let days = total_secs / 86400;
58    let hours = (total_secs % 86400) / 3600;
59    let minutes = (total_secs % 3600) / 60;
60    let secs = total_secs % 60;
61
62    let mut parts = Vec::new();
63    if days > 0 {
64        parts.push(format!("{}d", days));
65    }
66    if hours > 0 {
67        parts.push(format!("{}h", hours));
68    }
69    if minutes > 0 {
70        parts.push(format!("{}m", minutes));
71    }
72    if secs > 0 && days == 0 {
73        parts.push(format!("{}s", secs));
74    }
75
76    if parts.is_empty() {
77        "now".into()
78    } else {
79        parts.join(" ")
80    }
81}
82
83/// Format a frequency as a human-readable string.
84/// E.g., 3.5 per hour, 0.2 per minute, etc.
85pub fn prettyfrequency(freq: f64) -> String {
86    if freq <= 0.0 {
87        return "none".into();
88    }
89    // freq is in per-second
90    let per_minute = freq * 60.0;
91    let per_hour = freq * 3600.0;
92
93    if per_hour < 1.0 {
94        format!("{:.2}/h", per_hour)
95    } else if per_minute < 1.0 {
96        format!("{:.1}/h", per_hour)
97    } else {
98        format!("{:.1}/m", per_minute)
99    }
100}
101
102/// RFC 4648 Base32 encoding (standard alphabet, with padding).
103pub fn base32_encode(data: &[u8]) -> String {
104    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
105    let mut result = String::new();
106    let mut bits: u32 = 0;
107    let mut num_bits: u32 = 0;
108
109    for &byte in data {
110        bits = (bits << 8) | byte as u32;
111        num_bits += 8;
112        while num_bits >= 5 {
113            num_bits -= 5;
114            result.push(ALPHABET[((bits >> num_bits) & 0x1F) as usize] as char);
115        }
116    }
117    if num_bits > 0 {
118        result.push(ALPHABET[((bits << (5 - num_bits)) & 0x1F) as usize] as char);
119    }
120    // Pad to multiple of 8
121    while result.len() % 8 != 0 {
122        result.push('=');
123    }
124    result
125}
126
127/// RFC 4648 Base32 decoding (standard alphabet, with padding).
128pub fn base32_decode(s: &str) -> Option<Vec<u8>> {
129    let s = s.trim_end_matches('=');
130    let mut result = Vec::new();
131    let mut bits: u32 = 0;
132    let mut num_bits: u32 = 0;
133
134    for c in s.chars() {
135        let val = match c {
136            'A'..='Z' => c as u32 - 'A' as u32,
137            'a'..='z' => c as u32 - 'a' as u32,
138            '2'..='7' => c as u32 - '2' as u32 + 26,
139            _ => return None,
140        };
141        bits = (bits << 5) | val;
142        num_bits += 5;
143        if num_bits >= 8 {
144            num_bits -= 8;
145            result.push((bits >> num_bits) as u8);
146        }
147    }
148    Some(result)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_size_str() {
157        assert_eq!(size_str(0), "0 B");
158        assert_eq!(size_str(500), "500 B");
159        assert_eq!(size_str(1234), "1.23 KB");
160        assert_eq!(size_str(1234567), "1.23 MB");
161        assert_eq!(size_str(1234567890), "1.23 GB");
162    }
163
164    #[test]
165    fn test_speed_str() {
166        assert_eq!(speed_str(500), "500 b/s");
167        assert_eq!(speed_str(10_000_000), "10.00 Mb/s");
168        assert_eq!(speed_str(1_000_000), "1.00 Mb/s");
169    }
170
171    #[test]
172    fn test_prettyhexrep() {
173        assert_eq!(prettyhexrep(&[0xab, 0xcd, 0xef]), "abcdef");
174        assert_eq!(prettyhexrep(&[0x00, 0xff]), "00ff");
175    }
176
177    #[test]
178    fn test_prettytime() {
179        assert_eq!(prettytime(0.0), "now");
180        assert_eq!(prettytime(30.0), "30s");
181        assert_eq!(prettytime(90.0), "1m 30s");
182        assert_eq!(prettytime(3661.0), "1h 1m 1s");
183        assert_eq!(prettytime(86400.0), "1d");
184        assert_eq!(prettytime(90061.0), "1d 1h 1m");
185    }
186
187    #[test]
188    fn test_prettyfrequency() {
189        assert_eq!(prettyfrequency(0.0), "none");
190        assert_eq!(prettyfrequency(-1.0), "none");
191        // 1 per hour = 1/3600 per second
192        assert_eq!(prettyfrequency(1.0 / 3600.0), "1.0/h");
193        // 10 per minute = 10/60 per second
194        assert_eq!(prettyfrequency(10.0 / 60.0), "10.0/m");
195    }
196
197    #[test]
198    fn test_base32_encode() {
199        assert_eq!(base32_encode(b""), "");
200        assert_eq!(base32_encode(b"f"), "MY======");
201        assert_eq!(base32_encode(b"fo"), "MZXQ====");
202        assert_eq!(base32_encode(b"foo"), "MZXW6===");
203        assert_eq!(base32_encode(b"foob"), "MZXW6YQ=");
204        assert_eq!(base32_encode(b"fooba"), "MZXW6YTB");
205        assert_eq!(base32_encode(b"foobar"), "MZXW6YTBOI======");
206    }
207
208    #[test]
209    fn test_base32_decode() {
210        assert_eq!(base32_decode("").unwrap(), b"");
211        assert_eq!(base32_decode("MY======").unwrap(), b"f");
212        assert_eq!(base32_decode("MZXQ====").unwrap(), b"fo");
213        assert_eq!(base32_decode("MZXW6===").unwrap(), b"foo");
214        assert_eq!(base32_decode("MZXW6YQ=").unwrap(), b"foob");
215        assert_eq!(base32_decode("MZXW6YTB").unwrap(), b"fooba");
216        assert_eq!(base32_decode("MZXW6YTBOI======").unwrap(), b"foobar");
217        // Case insensitive
218        assert_eq!(base32_decode("mzxw6===").unwrap(), b"foo");
219        // Invalid char
220        assert!(base32_decode("!!!").is_none());
221    }
222
223    #[test]
224    fn test_base32_roundtrip() {
225        let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03];
226        let encoded = base32_encode(&data);
227        let decoded = base32_decode(&encoded).unwrap();
228        assert_eq!(decoded, data);
229    }
230}