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