Skip to main content

ort_openrouter_cli/common/
utils.rs

1//! ort: Open Router CLI
2//! https://github.com/grahamking/ort
3//!
4//! MIT License
5//! Copyright (c) 2025 Graham King
6
7extern crate alloc;
8use alloc::ffi::CString;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11
12use core::ffi::{c_str::CStr, c_void};
13
14use crate::libc;
15
16/// Converts the number to a string, putting it plus a carriage return into `buf`.
17/// `buf` must be big enough to hold the largest possible number of digits in
18/// your number + 2 ('\n' and '\0').
19/// Returns the length of the converted string, including null bute.
20pub(crate) fn to_ascii(mut num: usize, buf: &mut [u8]) -> usize {
21    if num == 0 {
22        buf[0] = b'0';
23        buf[1] = 0;
24        return 2;
25    }
26
27    let mut div = 1usize;
28    while num / div >= 10 {
29        div *= 10;
30    }
31
32    let mut i = 0usize;
33    while div != 0 {
34        buf[i] = b'0' + (num / div) as u8;
35        num %= div;
36        div /= 10;
37        i += 1;
38    }
39    buf[i] = b'\n';
40    i += 1;
41    buf[i] = 0;
42    i + 1
43}
44
45pub(crate) fn num_to_string(mut num: usize) -> String {
46    if num == 0 {
47        return "0".to_string();
48    }
49
50    let mut buf: [u8; 20] = [0; 20];
51    let mut div = 1usize;
52    while num / div >= 10 {
53        div *= 10;
54    }
55
56    let mut i = 0usize;
57    while div != 0 {
58        buf[i] = b'0' + (num / div) as u8;
59        num %= div;
60        div /= 10;
61        i += 1;
62    }
63
64    unsafe { String::from_utf8_unchecked(buf[..i].into()) }
65}
66
67/// Convert a float to it's string representation with given number of
68/// significant_digits after the decimal.
69pub(crate) fn float_to_string(mut f: f64, significant_digits: usize) -> String {
70    if f.is_nan() {
71        return "NaN".into();
72    }
73    if f.is_infinite() {
74        return if f < 0.0 { "-inf".into() } else { "inf".into() };
75    }
76
77    let mut result = String::new();
78
79    if f < 0.0 {
80        result.push('-');
81        f = -f;
82    }
83
84    // Handle integer part
85    let mut integer_part = f as u64;
86    let mut fraction_part = f - (integer_part as f64);
87
88    // Naive integer to string conversion
89    if integer_part == 0 {
90        result.push('0');
91    } else {
92        let mut buffer = [0u8; 20]; // Max u64 is ~1.8e19
93        let mut idx = 0;
94        while integer_part > 0 {
95            buffer[idx] = (integer_part % 10) as u8 + b'0';
96            integer_part /= 10;
97            idx += 1;
98        }
99        while idx > 0 {
100            idx -= 1;
101            result.push(buffer[idx] as char);
102        }
103    }
104
105    if significant_digits > 0 {
106        result.push('.');
107
108        for _ in 0..significant_digits {
109            fraction_part *= 10.0;
110            let digit = fraction_part as u8; // Truncate
111            result.push((digit + b'0') as char);
112            fraction_part -= digit as f64;
113        }
114    }
115
116    result
117}
118
119#[allow(unused)]
120pub(crate) fn print_hex(prefix: &CStr, v: &[u8]) {
121    let hex: alloc::string::String = v
122        .iter()
123        .map(|b| alloc::format!(" {:02x}", b))
124        .collect::<Vec<_>>()
125        .join("");
126    print_string(prefix, &hex);
127}
128
129#[allow(unused)]
130pub(crate) fn print_string(prefix: &CStr, s: &str) {
131    let msg = CString::new(zclean(&mut s.to_string())).unwrap();
132    unsafe { libc::printf(c"%s%s\n".as_ptr(), prefix.as_ptr(), msg.as_ptr()) };
133}
134
135/// Replace any null bytes with an underscore, making it C-safe
136/// Makes this construction safe from panic: `CString::new(zclean(s)).unwrap()`
137pub(crate) fn zclean(s: &mut str) -> &str {
138    for byte in unsafe { s.as_bytes_mut() } {
139        if *byte == 0 {
140            *byte = b'_';
141        }
142    }
143    s
144}
145
146pub(crate) fn slug(s: &str) -> String {
147    let mut out = String::with_capacity(16);
148    out.extend(s.chars().map(|c| {
149        if c.is_alphanumeric() {
150            c.to_lowercase().next().unwrap_or('-')
151        } else {
152            '-'
153        }
154    }));
155    out
156}
157
158// The filename of the last invocation of `ort`, taking into account tmux pane ID.
159pub(crate) fn last_filename() -> String {
160    // We don't expect pane IDs to go beyong 999
161    let mut buf: [u8; 3] = [0; 3];
162    let buf_len = tmux_pane_id(&mut buf);
163
164    let mut out = String::with_capacity(16);
165    out.push_str("last-");
166    // safety: to_ascii only returns chars '0'-'9'.
167    out.push_str(unsafe { str::from_utf8_unchecked(&buf[..buf_len]) });
168    out.push_str(".json");
169
170    out
171}
172
173// Write the ID of this tmux pane as a string into the given buf.
174// Writes 0 if there is no TMUX_PANE env var defined.
175// Returns the length in bytes of the written ID.
176pub fn tmux_pane_id(buf: &mut [u8]) -> usize {
177    let v = get_env(c"TMUX_PANE");
178    if v.is_empty() {
179        buf[0] = b'0';
180        return 1;
181    }
182    // removing leading '%'. Values are e.g. '%4'
183    let id_len = v.count_bytes() - 1;
184    buf[..id_len].copy_from_slice(&v.to_bytes()[1..]);
185    id_len
186}
187
188/// Read the value of an environment variable
189// Can't use std::env, we're no_std
190pub(crate) fn get_env(cs: &CStr) -> &'static CStr {
191    // Get a pointer to the var in our process env (above _start)
192    let value_ptr = unsafe { libc::getenv(cs.as_ptr()) };
193    if value_ptr.is_null() {
194        return c"";
195    }
196    // Re-interpret those bytes as a Rust CStr
197    unsafe { CStr::from_ptr(value_ptr) }
198}
199
200/// Create this directory if necessary. Does not create ancestors.
201pub(crate) fn ensure_dir_exists(dir: &str) {
202    let cs = CString::new(dir).unwrap();
203    if !path_exists(cs.as_ref()) {
204        unsafe { libc::mkdir(cs.as_ptr(), 0o755) };
205    }
206}
207
208/// Does this file path exists, and is accessible by the user?
209pub(crate) fn path_exists(path: &CStr) -> bool {
210    unsafe { libc::access(path.as_ptr(), libc::F_OK) == 0 }
211}
212
213/// Read a file into memory
214pub(crate) fn filename_read_to_string(filename: &str) -> Result<String, &'static str> {
215    let cs = CString::new(filename).unwrap();
216    let fd = unsafe { libc::open(cs.as_ptr(), libc::O_RDONLY) };
217    if fd < 0 {
218        return Err("NOT FOUND");
219    }
220
221    let mut content = Vec::new();
222    let mut buffer = [0u8; 4096];
223
224    loop {
225        let bytes_read =
226            unsafe { libc::read(fd, buffer.as_mut_ptr() as *mut c_void, buffer.len()) };
227
228        if bytes_read < 0 {
229            let _ = unsafe { libc::close(fd) };
230            return Err("READ ERROR");
231        }
232        if bytes_read == 0 {
233            break;
234        }
235        let bytes_read = bytes_read as usize; // we checked, it's positive
236        content.extend_from_slice(&buffer[..bytes_read]);
237    }
238
239    let out = String::from_utf8_lossy(&content);
240    Ok(out.into_owned().to_string())
241}
242
243#[cfg(test)]
244mod tests {
245    use super::float_to_string;
246
247    #[test]
248    fn nan_and_infinity() {
249        assert_eq!(float_to_string(f64::NAN, 3), "NaN");
250        assert_eq!(float_to_string(f64::INFINITY, 3), "inf");
251        assert_eq!(float_to_string(f64::NEG_INFINITY, 3), "-inf");
252    }
253
254    #[test]
255    fn sign_and_integer_part() {
256        assert_eq!(float_to_string(-2.5, 1), "-2.5");
257        assert_eq!(float_to_string(0.0, 0), "0");
258        assert_eq!(float_to_string(-0.0, 2), "0.00"); // f < 0.0 is false for -0.0
259        assert_eq!(float_to_string(12345.0, 0), "12345");
260    }
261
262    #[test]
263    fn fractional_digits_truncate_not_round() {
264        // 1.875 is exactly representable; with 2 digits -> "1.87" (truncation)
265        assert_eq!(float_to_string(1.875, 2), "1.87");
266    }
267
268    #[test]
269    fn fractional_leading_zeros() {
270        // 1/64 = 0.015625 is exactly representable
271        assert_eq!(float_to_string(0.015625, 3), "0.015");
272    }
273
274    #[test]
275    fn no_decimal_point_when_zero_digits() {
276        assert_eq!(float_to_string(3.75, 0), "3");
277    }
278}