1pub mod config;
14pub mod log_store;
15
16pub use lds_session::{CoreError, LdsState, Session, SessionConfig, SessionError};
17
18use std::path::PathBuf;
19
20pub fn find_in_path(binary: &str) -> Option<PathBuf> {
28 let path = std::env::var_os("PATH")?;
29 for dir in std::env::split_paths(&path) {
30 let candidate = dir.join(binary);
31 if candidate.is_file() {
32 return Some(candidate);
33 }
34 }
35 None
36}
37
38#[derive(Debug, Clone, serde::Serialize)]
40pub struct BinaryStatus {
41 pub name: String,
42 pub available: bool,
43 pub path: Option<String>,
44}
45
46pub fn check_binaries(names: &[&str]) -> Vec<BinaryStatus> {
48 names
49 .iter()
50 .map(|name| {
51 let resolved = find_in_path(name);
52 BinaryStatus {
53 name: (*name).to_string(),
54 available: resolved.is_some(),
55 path: resolved.map(|p| p.display().to_string()),
56 }
57 })
58 .collect()
59}
60
61pub fn truncate_output(raw: &[u8], max: usize) -> (String, bool) {
67 if raw.len() <= max {
68 return (String::from_utf8_lossy(raw).into_owned(), false);
69 }
70 let half = max / 2;
71 let head_end = find_utf8_boundary(raw, half);
72 let tail_start = find_utf8_boundary_rev(raw, raw.len() - half);
73 let head = String::from_utf8_lossy(&raw[..head_end]);
74 let tail = String::from_utf8_lossy(&raw[tail_start..]);
75 let mut out = head.into_owned();
76 out.push_str(&format!(
77 "\n\n--- [truncated: {} bytes total, showing first/last ~{} bytes] ---\n\n",
78 raw.len(),
79 half,
80 ));
81 out.push_str(&tail);
82 (out, true)
83}
84
85fn find_utf8_boundary(buf: &[u8], pos: usize) -> usize {
86 let pos = pos.min(buf.len());
87 let mut i = pos;
88 while i > 0 && !is_utf8_char_start(buf[i]) {
89 i -= 1;
90 }
91 i
92}
93
94fn find_utf8_boundary_rev(buf: &[u8], pos: usize) -> usize {
95 let pos = pos.min(buf.len());
96 let mut i = pos;
97 while i < buf.len() && !is_utf8_char_start(buf[i]) {
98 i += 1;
99 }
100 i
101}
102
103fn is_utf8_char_start(b: u8) -> bool {
104 (b & 0xC0) != 0x80
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn truncate_short_input() {
114 let data = b"hello world";
115 let (out, truncated) = truncate_output(data, 200);
116 assert_eq!(out, "hello world");
117 assert!(!truncated);
118 }
119
120 #[test]
121 fn truncate_empty() {
122 let (out, truncated) = truncate_output(b"", 100);
123 assert_eq!(out, "");
124 assert!(!truncated);
125 }
126
127 #[test]
128 fn truncate_over_limit() {
129 let data: Vec<u8> = (0..1000).map(|i| b'A' + (i % 26) as u8).collect();
130 let (out, truncated) = truncate_output(&data, 100);
131 assert!(truncated);
132 assert!(out.contains("[truncated:"));
133 assert!(out.len() < data.len());
134 }
135
136 #[test]
137 fn truncate_multibyte_boundary() {
138 let data = "あいうえお".as_bytes(); let (out, truncated) = truncate_output(data, 10);
141 assert!(truncated);
142 assert!(out.is_ascii() || out.chars().all(|c| c.len_utf8() > 0));
144 }
145
146 #[test]
147 fn truncate_exact_limit() {
148 let data = b"exactly ten";
149 let (out, truncated) = truncate_output(data, data.len());
150 assert_eq!(out, "exactly ten");
151 assert!(!truncated);
152 }
153
154 #[test]
155 fn find_in_path_resolves_common_binary() {
156 let resolved = find_in_path("sh");
158 assert!(resolved.is_some(), "sh should be on PATH");
159 assert!(resolved.unwrap().is_file());
160 }
161
162 #[test]
163 fn find_in_path_returns_none_for_unknown() {
164 assert!(find_in_path("definitely-not-a-real-binary-xyz-12345").is_none());
165 }
166
167 #[test]
168 fn check_binaries_marks_missing() {
169 let report = check_binaries(&["sh", "definitely-not-a-real-binary-xyz-12345"]);
170 assert_eq!(report.len(), 2);
171 assert_eq!(report[0].name, "sh");
172 assert!(report[0].available);
173 assert!(report[0].path.is_some());
174 assert_eq!(report[1].name, "definitely-not-a-real-binary-xyz-12345");
175 assert!(!report[1].available);
176 assert!(report[1].path.is_none());
177 }
178}