1use std::io::{Cursor, Write};
21use std::path::{Path, PathBuf};
22use std::sync::{Arc, Mutex};
23
24use serde_json::{json, Value};
25
26use crate::protocol::*;
27use crate::sdk::{serve, Host, Plugin};
28
29#[derive(Debug, Default)]
31pub struct Output {
32 pub messages: Vec<PluginMessage>,
33 pub exit_code: i32,
35}
36
37impl Output {
38 pub fn ready(&self) -> bool {
40 self.messages.iter().any(|m| matches!(m, PluginMessage::Ready))
41 }
42 pub fn results(&self) -> Vec<(String, Outcome, String, String)> {
44 self.messages
45 .iter()
46 .filter_map(|m| match m {
47 PluginMessage::CallResult { path, outcome, message, url } => Some((path.clone(), *outcome, message.clone(), url.clone())),
48 _ => None,
49 })
50 .collect()
51 }
52 pub fn logs(&self) -> Vec<(Level, String)> {
54 self.messages
55 .iter()
56 .filter_map(|m| match m {
57 PluginMessage::Log { level, message } => Some((*level, message.clone())),
58 _ => None,
59 })
60 .collect()
61 }
62 pub fn metrics(&self) -> Vec<Metrics> {
64 self.messages
65 .iter()
66 .filter_map(|m| match m {
67 PluginMessage::Metrics(x) => Some(x.clone()),
68 _ => None,
69 })
70 .collect()
71 }
72 pub fn status(&self) -> Option<(State, String)> {
74 self.messages.iter().rev().find_map(|m| match m {
75 PluginMessage::Status { state, message } => Some((*state, message.clone())),
76 _ => None,
77 })
78 }
79}
80
81#[derive(Clone, Default)]
82struct Shared(Arc<Mutex<Vec<u8>>>);
83
84impl Write for Shared {
85 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
86 self.0.lock().unwrap().extend_from_slice(b);
87 Ok(b.len())
88 }
89 fn flush(&mut self) -> std::io::Result<()> {
90 Ok(())
91 }
92}
93
94pub fn run<P: Plugin>(messages: impl IntoIterator<Item = HostMessage>) -> Output {
97 let mut input = String::new();
98 for m in messages {
99 input.push_str(&serde_json::to_string(&m).expect("serializable"));
100 input.push('\n');
101 }
102 let buf = Shared::default();
103 let exit_code = serve::<P>(Cursor::new(input), Host::to_writer(buf.clone()));
104 let bytes = buf.0.lock().unwrap().clone();
105 parse(&bytes, exit_code)
106}
107
108pub fn capture() -> (Host, Captured) {
111 let buf = Shared::default();
112 (Host::to_writer(buf.clone()), Captured(buf))
113}
114
115pub struct Captured(Shared);
117
118impl Captured {
119 pub fn output(&self) -> Output {
120 parse(&self.0 .0.lock().unwrap(), 0)
121 }
122}
123
124fn parse(bytes: &[u8], exit_code: i32) -> Output {
125 let messages = String::from_utf8_lossy(bytes).lines().filter_map(|l| serde_json::from_str(l).ok()).collect();
126 Output { messages, exit_code }
127}
128
129pub fn temp_dir(name: &str) -> PathBuf {
131 use std::sync::atomic::{AtomicU32, Ordering};
132 static N: AtomicU32 = AtomicU32::new(0);
133 let d = std::env::temp_dir().join(format!("{name}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
134 let _ = std::fs::remove_dir_all(&d);
135 std::fs::create_dir_all(&d).expect("temp dir");
136 d
137}
138
139pub fn hello(dir: &Path, config: Value) -> Hello {
144 let data_dir = dir.join("data");
145 let _ = std::fs::create_dir_all(&data_dir);
146 Hello {
147 api: API_VERSION,
148 host: HostInfo { name: "test".into(), version: "0".into() },
149 config,
150 systems: vec![SystemInfo { index: 0, short_name: "sys1".into(), kind: "p25".into(), config: Value::Null }],
151 capture_dir: dir.to_path_buf(),
152 data_dir,
153 audio_formats: vec!["wav".into(), format::M4A.into()],
154 }
155}
156
157pub fn call(dir: &Path, short_name: &str, tg: u32) -> ConcludedCall {
161 let start = 1_700_000_000 + tg as i64;
162 let path = format!("{short_name}/2023/11/14/{tg}-{start}_851012500");
163 let base = dir.join(&path);
164 std::fs::create_dir_all(base.parent().unwrap()).expect("call dir");
165 let record = json!({
166 "call_num": tg, "short_name": short_name, "talkgroup": tg, "talkgroup_tag": format!("TG {tg}"),
167 "freq": 851012500u64, "start_time": start, "stop_time": start + 3, "call_length": 3,
168 "emergency": 0, "encrypted": 0, "audio_type": "digital",
169 "freqList": [{ "freq": 851012500u64, "time": start, "pos": 0, "len": 3, "error_count": 2, "spike_count": 0 }],
170 "srcList": [{ "src": 1234, "time": start, "pos": 0, "emergency": 0, "signal_system": "", "tag": "", "tag_ota": "" }],
171 });
172 let files = CallFiles { json: ext(&base, "json"), wav: ext(&base, "wav"), m4a: Some(ext(&base, "m4a")) };
173 std::fs::write(&files.json, record.to_string()).expect("json");
174 std::fs::write(&files.wav, silent_wav(8000, 3)).expect("wav");
175 std::fs::write(files.m4a.as_ref().unwrap(), b"\0\0\0\x18ftypM4A test").expect("m4a");
176 ConcludedCall { path, system: 0, call: serde_json::from_value(record).expect("record"), files }
177}
178
179fn ext(base: &Path, e: &str) -> PathBuf {
180 PathBuf::from(format!("{}.{e}", base.display()))
181}
182
183fn silent_wav(rate: u32, secs: u32) -> Vec<u8> {
184 let data = rate * secs * 2;
185 let mut w = Vec::with_capacity(44 + data as usize);
186 w.extend_from_slice(b"RIFF");
187 w.extend_from_slice(&(36 + data).to_le_bytes());
188 w.extend_from_slice(b"WAVEfmt ");
189 w.extend_from_slice(&16u32.to_le_bytes());
190 w.extend_from_slice(&1u16.to_le_bytes());
191 w.extend_from_slice(&1u16.to_le_bytes());
192 w.extend_from_slice(&rate.to_le_bytes());
193 w.extend_from_slice(&(rate * 2).to_le_bytes());
194 w.extend_from_slice(&2u16.to_le_bytes());
195 w.extend_from_slice(&16u16.to_le_bytes());
196 w.extend_from_slice(b"data");
197 w.extend_from_slice(&data.to_le_bytes());
198 w.resize(44 + data as usize, 0);
199 w
200}
201
202#[derive(Clone, Debug, Default)]
204pub struct Request {
205 pub method: String,
206 pub path: String,
208 pub headers: Vec<(String, String)>,
210 pub body: Vec<u8>,
211}
212
213impl Request {
214 pub fn header(&self, name: &str) -> Option<&str> {
215 let name = name.to_ascii_lowercase();
216 self.headers.iter().find(|(k, _)| *k == name).map(|(_, v)| v.as_str())
217 }
218
219 pub fn form_field(&self, name: &str) -> Option<Vec<u8>> {
221 self.form_part(name).map(|(_, v)| v)
222 }
223
224 pub fn form_file_name(&self, name: &str) -> Option<String> {
226 let (headers, _) = self.form_part(name)?;
227 let i = headers.find("filename=\"")? + 10;
228 Some(headers[i..].split('"').next()?.to_string())
229 }
230
231 fn form_part(&self, name: &str) -> Option<(String, Vec<u8>)> {
232 let ct = self.header("content-type")?;
233 let boundary = format!("--{}", ct.split("boundary=").nth(1)?.trim_matches('"'));
234 let b = &self.body;
235 let mut at = find(b, boundary.as_bytes(), 0)?;
236 loop {
237 let start = at + boundary.len() + 2;
238 let next = find(b, boundary.as_bytes(), start)?;
239 let part = &b[start..next.saturating_sub(2)];
240 let split = find(part, b"\r\n\r\n", 0)?;
241 let headers = String::from_utf8_lossy(&part[..split]).to_string();
242 if headers.contains(&format!("name=\"{name}\"")) {
243 return Some((headers, part[split + 4..].to_vec()));
244 }
245 at = next;
246 }
247 }
248}
249
250fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
251 hay.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|i| i + from)
252}
253
254type Respond = dyn Fn(&Request) -> (u16, String) + Send + Sync;
255
256pub struct MockServer {
266 url: String,
267 requests: Arc<Mutex<Vec<Request>>>,
268}
269
270impl MockServer {
271 pub fn start(respond: impl Fn(&Request) -> (u16, String) + Send + Sync + 'static) -> MockServer {
272 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
273 let url = format!("http://{}", listener.local_addr().expect("addr"));
274 let requests = Arc::new(Mutex::new(Vec::new()));
275 let (reqs, respond): (_, Arc<Respond>) = (requests.clone(), Arc::new(respond));
276 std::thread::spawn(move || {
277 for stream in listener.incoming().flatten() {
278 let (reqs, respond) = (reqs.clone(), respond.clone());
279 std::thread::spawn(move || serve_one(stream, &reqs, &*respond));
280 }
281 });
282 MockServer { url, requests }
283 }
284
285 pub fn url(&self) -> &str {
287 &self.url
288 }
289
290 pub fn requests(&self) -> Vec<Request> {
292 self.requests.lock().unwrap().clone()
293 }
294}
295
296fn serve_one(mut s: std::net::TcpStream, reqs: &Mutex<Vec<Request>>, respond: &Respond) {
297 use std::io::Read;
298 let mut buf = Vec::new();
299 let mut chunk = [0u8; 16384];
300 let head_end = loop {
302 match s.read(&mut chunk) {
303 Ok(0) | Err(_) => return,
304 Ok(n) => buf.extend_from_slice(&chunk[..n]),
305 }
306 if let Some(i) = find(&buf, b"\r\n\r\n", 0) {
307 break i;
308 }
309 };
310 let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
311 let mut lines = head.split("\r\n");
312 let mut first = lines.next().unwrap_or("").split(' ');
313 let (method, path) = (first.next().unwrap_or("").to_string(), first.next().unwrap_or("").to_string());
314 let headers: Vec<(String, String)> = lines.filter_map(|l| l.split_once(':')).map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string())).collect();
315 let len: usize = headers.iter().find(|(k, _)| k == "content-length").and_then(|(_, v)| v.parse().ok()).unwrap_or(0);
316 let mut body = buf[head_end + 4..].to_vec();
317 while body.len() < len {
318 match s.read(&mut chunk) {
319 Ok(0) | Err(_) => break,
320 Ok(n) => body.extend_from_slice(&chunk[..n]),
321 }
322 }
323 let req = Request { method, path, headers, body };
324 let (status, text) = respond(&req);
325 reqs.lock().unwrap().push(req);
326 let resp = format!("HTTP/1.1 {status} X\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{text}", text.len());
327 let _ = s.write_all(resp.as_bytes());
328}