Skip to main content

rns_cli/
format.rs

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