ort_openrouter_cli/common/
utils.rs1extern 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::cli::Env;
15use crate::syscall;
16
17pub(crate) fn to_ascii(mut num: usize, buf: &mut [u8]) -> usize {
22 if num == 0 {
23 buf[0] = b'0';
24 buf[1] = 0;
25 return 2;
26 }
27
28 let mut div = 1usize;
29 while num / div >= 10 {
30 div *= 10;
31 }
32
33 let mut i = 0usize;
34 while div != 0 {
35 buf[i] = b'0' + (num / div) as u8;
36 num %= div;
37 div /= 10;
38 i += 1;
39 }
40 buf[i] = b'\n';
41 i += 1;
42 buf[i] = 0;
43 i + 1
44}
45
46pub fn num_to_string<T>(num: T) -> String
47where
48 T: TryInto<i128> + Copy,
49{
50 let Ok(mut num) = num.try_into() else {
51 panic!("num_to_string only supports values that fit in i128");
52 };
53
54 if num == 0 {
55 return "0".to_string();
56 }
57
58 let negative = num < 0;
59 if negative {
60 num = -num;
61 }
62
63 let mut buf: [u8; 40] = [0; 40];
64 let mut div = 1i128;
65 let mut i = 0usize;
66
67 if negative {
68 buf[i] = b'-';
69 i += 1;
70 }
71
72 while num / div >= 10 {
73 div *= 10;
74 }
75
76 while div != 0 {
77 buf[i] = b'0' + (num / div) as u8;
78 num %= div;
79 div /= 10;
80 i += 1;
81 }
82
83 unsafe { String::from_utf8_unchecked(buf[..i].into()) }
84}
85
86pub(crate) fn float_to_string(mut f: f64, significant_digits: usize) -> String {
89 if f.is_nan() {
90 return "NaN".into();
91 }
92 if f.is_infinite() {
93 return if f < 0.0 { "-inf".into() } else { "inf".into() };
94 }
95
96 let mut result = String::new();
97
98 if f < 0.0 {
99 result.push('-');
100 f = -f;
101 }
102
103 let mut integer_part = f as u64;
105 let mut fraction_part = f - (integer_part as f64);
106
107 if integer_part == 0 {
109 result.push('0');
110 } else {
111 let mut buffer = [0u8; 20]; let mut idx = 0;
113 while integer_part > 0 {
114 buffer[idx] = (integer_part % 10) as u8 + b'0';
115 integer_part /= 10;
116 idx += 1;
117 }
118 while idx > 0 {
119 idx -= 1;
120 result.push(buffer[idx] as char);
121 }
122 }
123
124 if significant_digits > 0 {
125 result.push('.');
126
127 for _ in 0..significant_digits {
128 fraction_part *= 10.0;
129 let digit = fraction_part as u8; result.push((digit + b'0') as char);
131 fraction_part -= digit as f64;
132 }
133 }
134
135 result
136}
137
138#[allow(unused)]
139pub(crate) fn print_hex(prefix: &CStr, v: &[u8]) {
140 let hex: alloc::string::String = v
141 .iter()
142 .map(|b| alloc::format!(" {:02x}", b))
143 .collect::<Vec<_>>()
144 .join("");
145 print_string(prefix, &hex);
146}
147
148#[allow(unused)]
149pub fn print_string(prefix: &CStr, s: &str) {
150 let msg = CString::new(zclean(&mut s.to_string())).unwrap();
151 let _ = syscall::write(1, prefix.as_ptr().cast::<c_void>(), prefix.count_bytes());
152 let _ = syscall::write(1, msg.as_ptr().cast::<c_void>(), msg.count_bytes());
153 let _ = syscall::write(1, c"\n".as_ptr().cast::<c_void>(), c"\n".count_bytes());
154}
155
156pub(crate) fn zclean(s: &mut str) -> &str {
159 for byte in unsafe { s.as_bytes_mut() } {
160 if *byte == 0 {
161 *byte = b'_';
162 }
163 }
164 s
165}
166
167pub(crate) fn slug(s: &str) -> String {
168 let mut out = String::with_capacity(16);
169 out.extend(s.chars().map(|c| {
170 if c.is_alphanumeric() {
171 c.to_lowercase().next().unwrap_or('-')
172 } else {
173 '-'
174 }
175 }));
176 out
177}
178
179pub(crate) fn last_filename(env: &Env) -> String {
181 let mut buf: [u8; 3] = [0; 3];
183 let buf_len = tmux_pane_id(env.TMUX_PANE.unwrap_or_default(), &mut buf);
184
185 let mut out = String::with_capacity(16);
186 out.push_str("last-");
187 out.push_str(unsafe { str::from_utf8_unchecked(&buf[..buf_len]) });
189 out.push_str(".json");
190
191 out
192}
193
194pub fn tmux_pane_id(tmux_pane_var: &str, buf: &mut [u8]) -> usize {
198 let v = tmux_pane_var;
199 if v.is_empty() {
200 buf[0] = b'0';
201 return 1;
202 }
203 let id_len = v.len() - 1;
205 buf[..id_len].copy_from_slice(&v.as_bytes()[1..]);
206 id_len
207}
208
209pub(crate) fn ensure_dir_exists(dir: &str) {
211 let cs = CString::new(dir).unwrap();
212 if !path_exists(cs.as_ref()) {
213 syscall::mkdir(cs.as_ptr(), 0o755);
214 }
215}
216
217pub(crate) fn path_exists(path: &CStr) -> bool {
219 syscall::access(path.as_ptr(), syscall::F_OK) == 0
220}
221
222pub(crate) fn filename_read_to_bytes(filename: &str) -> Result<Vec<u8>, &'static str> {
224 let cs = CString::new(filename).unwrap();
225 let fd = match syscall::open(cs.as_ptr(), syscall::O_RDONLY, 0) {
226 Ok(fd) => fd,
227 Err(v) if v == "Permission denied" => {
228 return Err(v);
229 }
230 Err(_) => {
231 return Err("NOT FOUND");
232 }
233 };
234
235 let mut content = Vec::new();
236 let mut buffer = [0u8; 4096];
237
238 loop {
239 let bytes_read = syscall::read(fd, buffer.as_mut_ptr() as *mut c_void, buffer.len());
240
241 if bytes_read < 0 {
242 let _ = syscall::close(fd);
243 return Err("READ ERROR");
244 }
245 if bytes_read == 0 {
246 break;
247 }
248 let bytes_read = bytes_read as usize; content.extend_from_slice(&buffer[..bytes_read]);
250 }
251
252 Ok(content)
253}
254
255pub(crate) fn filename_read_to_string(filename: &str) -> Result<String, &'static str> {
257 let content = filename_read_to_bytes(filename)?;
258 let out = String::from_utf8_lossy(&content);
259 Ok(out.into_owned().to_string())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::{float_to_string, num_to_string};
265
266 #[test]
267 fn num_to_string_handles_sign() {
268 assert_eq!(num_to_string(-42), "-42");
269 assert_eq!(num_to_string(42usize), "42");
270 assert_eq!(num_to_string(0), "0");
271 }
272
273 #[test]
274 fn nan_and_infinity() {
275 assert_eq!(float_to_string(f64::NAN, 3), "NaN");
276 assert_eq!(float_to_string(f64::INFINITY, 3), "inf");
277 assert_eq!(float_to_string(f64::NEG_INFINITY, 3), "-inf");
278 }
279
280 #[test]
281 fn sign_and_integer_part() {
282 assert_eq!(float_to_string(-2.5, 1), "-2.5");
283 assert_eq!(float_to_string(0.0, 0), "0");
284 assert_eq!(float_to_string(-0.0, 2), "0.00"); assert_eq!(float_to_string(12345.0, 0), "12345");
286 }
287
288 #[test]
289 fn fractional_digits_truncate_not_round() {
290 assert_eq!(float_to_string(1.875, 2), "1.87");
292 }
293
294 #[test]
295 fn fractional_leading_zeros() {
296 assert_eq!(float_to_string(0.015625, 3), "0.015");
298 }
299
300 #[test]
301 fn no_decimal_point_when_zero_digits() {
302 assert_eq!(float_to_string(3.75, 0), "3");
303 }
304}