agentmux_srv\backend\utilfn/
strutil.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5
6use std::io;
7use std::path::Path;
8
9// ---- Binary Detection ----
10
11/// Check if data contains binary (non-text) bytes.
12/// Returns true if any byte < 32 other than `\n`, `\r`, `\t`, `\f`, `\b`.
13pub fn has_binary_data(data: &[u8]) -> bool {
14    data.iter().any(|&b| b < 32 && b != b'\n' && b != b'\r' && b != b'\t' && b != 0x0C && b != 0x08)
15}
16
17/// Check if content is binary by examining up to 8192 bytes.
18/// Checks for null byte ratio > 1% and UTF-8 validity.
19pub fn is_binary_content(data: &[u8]) -> bool {
20    if data.is_empty() {
21        return false;
22    }
23    let sample_size = std::cmp::min(8192, data.len());
24    let sample = &data[..sample_size];
25
26    let null_count = sample.iter().filter(|&&b| b == 0).count();
27    if null_count as f64 / sample.len() as f64 > 0.01 {
28        return true;
29    }
30
31    if std::str::from_utf8(sample).is_err() {
32        return true;
33    }
34
35    false
36}
37
38// ---- Star Matching ----
39
40/// Match a delimited string with a pattern string.
41/// `*` matches a single part, `**` matches the rest of the string (only valid at end).
42pub fn star_match_string(pattern: &str, s: &str, delimiter: &str) -> bool {
43    let pattern_parts: Vec<&str> = pattern.split(delimiter).collect();
44    let string_parts: Vec<&str> = s.split(delimiter).collect();
45    let p_len = pattern_parts.len();
46    let s_len = string_parts.len();
47
48    for i in 0..p_len {
49        if pattern_parts[i] == "**" {
50            return i == p_len - 1;
51        }
52        if i >= s_len {
53            return false;
54        }
55        if pattern_parts[i] != "*" && pattern_parts[i] != string_parts[i] {
56            return false;
57        }
58    }
59    p_len == s_len
60}
61
62// ---- Slice Operations ----
63
64/// Find the index of an element in a slice. Returns -1 if not found.
65pub fn slice_idx<T: PartialEq>(arr: &[T], elem: &T) -> i32 {
66    for (idx, e) in arr.iter().enumerate() {
67        if e == elem {
68            return idx as i32;
69        }
70    }
71    -1
72}
73
74/// Remove an element from a vec. Returns a new vec without the element.
75pub fn remove_elem<T: PartialEq + Clone>(arr: &[T], elem: &T) -> Vec<T> {
76    let idx = slice_idx(arr, elem);
77    if idx == -1 {
78        return arr.to_vec();
79    }
80    let idx = idx as usize;
81    let mut result = Vec::with_capacity(arr.len() - 1);
82    result.extend_from_slice(&arr[..idx]);
83    result.extend_from_slice(&arr[idx + 1..]);
84    result
85}
86
87/// Add an element to a vec if it's not already present.
88pub fn add_elem_uniq<T: PartialEq + Clone>(arr: &[T], elem: T) -> Vec<T> {
89    if slice_idx(arr, &elem) != -1 {
90        return arr.to_vec();
91    }
92    let mut result = arr.to_vec();
93    result.push(elem);
94    result
95}
96
97/// Move element at `idx` to the front of the slice. Returns a new vec.
98pub fn move_to_front<T: Clone>(arr: &[T], idx: usize) -> Vec<T> {
99    if idx == 0 || idx >= arr.len() {
100        return arr.to_vec();
101    }
102    let mut rtn = Vec::with_capacity(arr.len());
103    rtn.push(arr[idx].clone());
104    rtn.extend_from_slice(&arr[..idx]);
105    rtn.extend_from_slice(&arr[idx + 1..]);
106    rtn
107}
108
109// ---- String Helpers ----
110
111/// Truncate a string with "..." if it exceeds maxLen (char-safe).
112pub fn ellipsis_str(s: &str, max_len: usize) -> String {
113    let max_len = if max_len < 4 { 4 } else { max_len };
114    let char_count = s.chars().count();
115    if char_count > max_len {
116        let truncated: String = s.chars().take(max_len - 3).collect();
117        format!("{}...", truncated)
118    } else {
119        s.to_string()
120    }
121}
122
123/// Truncate a string with "..." if it exceeds maxLen (char-safe).
124pub fn truncate_string(s: &str, max_len: usize) -> String {
125    let char_count = s.chars().count();
126    if char_count <= max_len {
127        return s.to_string();
128    }
129    let max_len = if max_len < 4 { 4 } else { max_len };
130    let truncated: String = s.chars().take(max_len - 3).collect();
131    format!("{}...", truncated)
132}
133
134/// Get the first line of a string.
135pub fn get_first_line(s: &str) -> &str {
136    match s.find('\n') {
137        Some(idx) => &s[..idx],
138        None => s,
139    }
140}
141
142/// Parse an integer from a string, returning 0 on error.
143pub fn atoi_no_err(s: &str) -> i32 {
144    s.parse::<i32>().unwrap_or(0)
145}
146
147/// Generate a random hex string of the given number of hex digits.
148pub fn random_hex_string(num_hex_digits: usize) -> Result<String, io::Error> {
149    use std::fmt::Write;
150    let num_bytes = num_hex_digits.div_ceil(2);
151    let mut bytes = vec![0u8; num_bytes];
152    getrandom(&mut bytes)?;
153    let mut hex = String::with_capacity(num_hex_digits);
154    for b in &bytes {
155        write!(hex, "{:02x}", b).unwrap();
156    }
157    hex.truncate(num_hex_digits);
158    Ok(hex)
159}
160
161/// Platform-independent random bytes.
162/// Uses /dev/urandom on Unix, hash-based randomness on Windows.
163fn getrandom(buf: &mut [u8]) -> Result<(), io::Error> {
164    #[cfg(unix)]
165    {
166        use std::fs::File;
167        use std::io::Read;
168        let mut f = File::open("/dev/urandom")?;
169        f.read_exact(buf)?;
170    }
171    #[cfg(windows)]
172    {
173        use std::collections::hash_map::RandomState;
174        use std::hash::{BuildHasher, Hasher};
175        let mut offset = 0;
176        while offset < buf.len() {
177            let state = RandomState::new();
178            let hash = state.build_hasher().finish();
179            let bytes = hash.to_ne_bytes();
180            let copy_len = std::cmp::min(bytes.len(), buf.len() - offset);
181            buf[offset..offset + copy_len].copy_from_slice(&bytes[..copy_len]);
182            offset += copy_len;
183        }
184    }
185    Ok(())
186}
187
188/// Convert known architecture names to standard patterns.
189pub fn filter_valid_arch(arch: &str) -> Result<&'static str, String> {
190    match arch.trim().to_lowercase().as_str() {
191        "amd64" | "x86_64" | "x64" => Ok("x64"),
192        "arm64" | "aarch64" => Ok("arm64"),
193        other => Err(format!("unknown architecture: {}", other)),
194    }
195}
196
197// ---- Atomic File Operations ----
198
199/// Atomically copy a file by writing to a temp file then renaming.
200/// On Unix, sets file permissions to `perms`. On Windows, permissions are ignored.
201pub fn atomic_rename_copy(dst_path: &Path, src_path: &Path, _perms: u32) -> io::Result<()> {
202    use std::fs;
203
204    let temp_name = format!("{}.new", dst_path.display());
205    let temp_path = Path::new(&temp_name);
206    fs::copy(src_path, temp_path)?;
207    #[cfg(unix)]
208    {
209        use std::os::unix::fs::PermissionsExt;
210        fs::set_permissions(temp_path, fs::Permissions::from_mode(_perms))?;
211    }
212    fs::rename(temp_path, dst_path)?;
213    Ok(())
214}
215
216/// Write file only if contents differ. Returns true if file was written.
217pub fn write_file_if_different(file_name: &Path, contents: &[u8]) -> io::Result<bool> {
218    use std::fs;
219    if let Ok(old_contents) = fs::read(file_name) {
220        if old_contents == contents {
221            return Ok(false);
222        }
223    }
224    fs::write(file_name, contents)?;
225    Ok(true)
226}
227
228// ---- Misc ----
229
230/// Get line and column from a byte offset in content.
231pub fn get_line_col_from_offset(data: &[u8], offset: usize) -> (usize, usize) {
232    let mut line = 1;
233    let mut col = 1;
234    for &byte in data.iter().take(std::cmp::min(offset, data.len())) {
235        if byte == b'\n' {
236            line += 1;
237            col = 1;
238        } else {
239            col += 1;
240        }
241    }
242    (line, col)
243}
244
245/// Find the longest common prefix of a set of strings, bounded by a root.
246pub fn longest_prefix(root: &str, strs: &[&str]) -> String {
247    if strs.is_empty() {
248        return root.to_string();
249    }
250    if strs.len() == 1 {
251        let comp = strs[0];
252        if comp.len() >= root.len() && comp.starts_with(root) {
253            return comp.to_string();
254        }
255    }
256    let mut lcp = strs[0].to_string();
257    for s in &strs[1..] {
258        let mut end = 0;
259        for (j, (a, b)) in lcp.chars().zip(s.chars()).enumerate() {
260            if a != b {
261                break;
262            }
263            end = j + a.len_utf8();
264        }
265        lcp.truncate(end);
266    }
267    if lcp.len() < root.len() || !lcp.starts_with(root) {
268        return root.to_string();
269    }
270    lcp
271}
272
273/// Indent each non-empty line of a string.
274pub fn indent_string(indent: &str, s: &str) -> String {
275    let mut result = String::new();
276    for line in s.split('\n') {
277        if line.is_empty() {
278            result.push('\n');
279        } else {
280            result.push_str(indent);
281            result.push_str(line);
282            result.push('\n');
283        }
284    }
285    result
286}
287
288/// Shell hex escape a string (each byte as `\xNN`).
289pub fn shell_hex_escape(s: &str) -> String {
290    let mut result = String::new();
291    for b in s.bytes() {
292        result.push_str(&format!("\\x{:02x}", b));
293    }
294    result
295}
296
297/// Combine two string arrays, removing duplicates while preserving order.
298pub fn combine_str_arrays(arr1: &[String], arr2: &[String]) -> Vec<String> {
299    let mut seen = std::collections::HashSet::new();
300    let mut result = Vec::new();
301    for s in arr1.iter().chain(arr2.iter()) {
302        if seen.insert(s.clone()) {
303            result.push(s.clone());
304        }
305    }
306    result
307}
308
309// ---- Sentinel / Helper Types ----
310
311/// Sentinel value for StrWithPos.pos to indicate no position.
312pub const NO_STR_POS: i32 = -1;
313
314/// A string with a cursor position (rune-based, not byte-based).
315#[derive(Debug, Clone, PartialEq)]
316pub struct StrWithPos {
317    pub str_val: String,
318    pub pos: i32,
319}
320
321impl StrWithPos {
322    pub fn new(s: String, pos: i32) -> Self {
323        Self { str_val: s, pos }
324    }
325
326    /// Parse a string with `[*]` cursor marker.
327    pub fn parse(s: &str) -> Self {
328        match s.find("[*]") {
329            None => Self { str_val: s.to_string(), pos: NO_STR_POS },
330            Some(idx) => {
331                let before = &s[..idx];
332                let after = &s[idx + 3..];
333                let pos = before.chars().count() as i32;
334                Self {
335                    str_val: format!("{}{}", before, after),
336                    pos,
337                }
338            }
339        }
340    }
341
342    pub fn prepend(&self, prefix: &str) -> Self {
343        Self {
344            str_val: format!("{}{}", prefix, self.str_val),
345            pos: prefix.chars().count() as i32 + self.pos,
346        }
347    }
348
349    pub fn append(&self, suffix: &str) -> Self {
350        Self {
351            str_val: format!("{}{}", self.str_val, suffix),
352            pos: self.pos,
353        }
354    }
355}
356
357impl std::fmt::Display for StrWithPos {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        if self.pos == NO_STR_POS {
360            write!(f, "{}", self.str_val)
361        } else if self.pos < 0 {
362            write!(f, "[*]_{}", self.str_val)
363        } else {
364            let pos = self.pos as usize;
365            let mut chars: Vec<char> = Vec::new();
366            for (i, ch) in self.str_val.chars().enumerate() {
367                if i == pos {
368                    chars.extend(['[', '*', ']']);
369                }
370                chars.push(ch);
371            }
372            if pos >= self.str_val.chars().count() {
373                chars.extend(['[', '*', ']']);
374            }
375            write!(f, "{}", chars.iter().collect::<String>())
376        }
377    }
378}