1use std::io::{BufReader, Read, Write};
2use std::path::PathBuf;
3use std::process::{Child, Command, ExitStatus, Stdio};
4use std::thread;
5use std::time::Duration;
6
7pub struct P4Cli {
20 bin_path: PathBuf,
21 _temp_dir: tempfile::TempDir,
22}
23
24pub struct P4Output {
26 exit_code: i32,
27 stdout: Vec<u8>,
28 stderr: Vec<u8>,
29}
30
31impl P4Output {
32 pub fn exit_code(&self) -> i32 {
33 self.exit_code
34 }
35
36 pub fn success(&self) -> bool {
37 self.exit_code == 0
38 }
39
40 pub fn stdout(&self) -> &[u8] {
41 &self.stdout
42 }
43
44 pub fn stderr(&self) -> &[u8] {
45 &self.stderr
46 }
47
48 pub fn stdout_str(&self) -> std::io::Result<&str> {
49 std::str::from_utf8(&self.stdout).map_err(std::io::Error::other)
50 }
51
52 pub fn stderr_str(&self) -> std::io::Result<&str> {
53 std::str::from_utf8(&self.stderr).map_err(std::io::Error::other)
54 }
55
56 pub fn stdout_lines(&self) -> std::io::Result<Vec<&str>> {
57 let s = self.stdout_str()?;
58 if s.is_empty() {
59 Ok(Vec::new())
60 } else {
61 Ok(s.lines().collect())
62 }
63 }
64
65 pub fn stderr_lines(&self) -> std::io::Result<Vec<&str>> {
66 let s = self.stderr_str()?;
67 if s.is_empty() {
68 Ok(Vec::new())
69 } else {
70 Ok(s.lines().collect())
71 }
72 }
73}
74
75impl std::fmt::Debug for P4Output {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("P4Output")
78 .field("exit_code", &self.exit_code)
79 .field("stdout_len", &self.stdout.len())
80 .field("stderr_len", &self.stderr.len())
81 .finish()
82 }
83}
84
85pub enum P4StreamEvent {
91 Stdout(Vec<u8>),
92 Stderr(Vec<u8>),
93 Exit(i32),
94}
95
96impl P4StreamEvent {
97 pub fn as_utf8(&self) -> Option<&str> {
99 match self {
100 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
101 std::str::from_utf8(data).ok()
102 }
103 P4StreamEvent::Exit(_) => None,
104 }
105 }
106}
107
108impl std::fmt::Display for P4StreamEvent {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
112 if let Ok(text) = std::str::from_utf8(data) {
113 write!(f, "{text}")
114 } else {
115 write!(f, "<{} bytes>", data.len())
116 }
117 }
118 P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
119 }
120 }
121}
122
123pub struct P4Stream {
150 rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
151 child: Option<Child>,
152 #[allow(dead_code)]
153 handles: Vec<thread::JoinHandle<()>>,
154 exhausted: bool,
155}
156
157impl Iterator for P4Stream {
158 type Item = std::io::Result<P4StreamEvent>;
159
160 fn next(&mut self) -> Option<Self::Item> {
161 if self.exhausted {
162 return None;
163 }
164 match self.rx.recv() {
165 Ok(item) => Some(item),
166 Err(_) => {
167 self.exhausted = true;
168 let code = self
169 .child
170 .take()
171 .and_then(|mut c| c.wait().ok())
172 .and_then(|s| s.code())
173 .unwrap_or(-1);
174 Some(Ok(P4StreamEvent::Exit(code)))
175 }
176 }
177 }
178}
179
180impl Drop for P4Stream {
181 fn drop(&mut self) {
182 if let Some(ref mut child) = self.child {
183 let _ = child.kill();
184 let _ = child.wait();
185 }
186 }
187}
188
189fn write_p4_cli_to_disk() -> std::io::Result<(PathBuf, tempfile::TempDir)> {
194 let zst_data = get_p4_cli_zst();
195 let binary_data = decompress_zst(&zst_data)?;
196
197 let temp_dir = create_temp_dir()?;
198 let bin_path = temp_dir.path().join("p4_binary");
199 let tmp_path = temp_dir.path().join(".tmp");
200
201 {
202 let mut file = std::fs::File::create(&tmp_path)?;
203 file.write_all(&binary_data)?;
204 file.sync_all()?;
205 }
206 std::fs::rename(&tmp_path, &bin_path)?;
207 set_executable_perms(&bin_path)?;
208
209 Ok((bin_path, temp_dir))
210}
211
212fn create_temp_dir() -> std::io::Result<tempfile::TempDir> {
213 let mut builder = tempfile::Builder::new();
214 builder.prefix("p4cli-20251");
215 #[cfg(unix)]
216 {
217 use std::os::unix::fs::PermissionsExt;
218 builder.permissions(std::fs::Permissions::from_mode(0o700));
219 }
220 builder.tempdir()
221}
222
223fn decompress_zst(zst_data: &[u8]) -> std::io::Result<Vec<u8>> {
224 let mut decoder = zstd::stream::Decoder::new(zst_data)?;
225 let mut buf = Vec::new();
226 std::io::copy(&mut decoder, &mut buf)?;
227 Ok(buf)
228}
229
230#[cfg(unix)]
231fn set_executable_perms(path: &std::path::Path) -> std::io::Result<()> {
232 use std::os::unix::fs::PermissionsExt;
233 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
234}
235
236#[cfg(not(unix))]
237fn set_executable_perms(_path: &std::path::Path) -> std::io::Result<()> {
238 Ok(())
239}
240
241fn get_p4_cli_zst() -> Vec<u8> {
242 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
243 {
244 use p4cli_20251_win_x64::get_p4_cli_zst;
245 get_p4_cli_zst()
246 }
247
248 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
249 {
250 use p4cli_20251_mac_arm64::get_p4_cli_zst;
251 get_p4_cli_zst()
252 }
253
254 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
255 {
256 use p4cli_20251_mac_x64::get_p4_cli_zst;
257 get_p4_cli_zst()
258 }
259
260 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
261 {
262 use p4cli_20251_linux_x64::get_p4_cli_zst;
263 get_p4_cli_zst()
264 }
265
266 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
267 {
268 use p4cli_20251_linux_arm64::get_p4_cli_zst;
269 get_p4_cli_zst()
270 }
271
272 #[cfg(not(any(
273 all(target_os = "windows", target_arch = "x86_64"),
274 all(target_os = "macos", target_arch = "aarch64"),
275 all(target_os = "macos", target_arch = "x86_64"),
276 all(target_os = "linux", target_arch = "x86_64"),
277 all(target_os = "linux", target_arch = "aarch64")
278 )))]
279 {
280 compile_error!(format!(
281 "Unsupported platform: {}-{}",
282 std::env::consts::OS,
283 std::env::consts::ARCH
284 ));
285 Vec::new()
286 }
287}
288
289pub struct P4Command<'a> {
295 cli: &'a P4Cli,
296 args: Vec<std::ffi::OsString>,
297 timeout: Option<Duration>,
298 cwd: Option<PathBuf>,
299 envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
300 stdin_data: Option<Vec<u8>>,
301}
302
303impl<'a> P4Command<'a> {
304 fn new(cli: &'a P4Cli) -> Self {
305 Self {
306 cli,
307 args: Vec::new(),
308 timeout: None,
309 cwd: None,
310 envs: Vec::new(),
311 stdin_data: None,
312 }
313 }
314
315 pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
316 self.args.push(arg.as_ref().to_os_string());
317 self
318 }
319
320 pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
321 self.args
322 .extend(args.iter().map(|a| a.as_ref().to_os_string()));
323 self
324 }
325
326 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
328 self.timeout = Some(timeout);
329 self
330 }
331
332 pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
333 self.cwd = Some(path.into());
334 self
335 }
336
337 pub fn env(
338 &mut self,
339 key: impl Into<std::ffi::OsString>,
340 val: impl Into<std::ffi::OsString>,
341 ) -> &mut Self {
342 self.envs.push((key.into(), val.into()));
343 self
344 }
345
346 pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
347 self.stdin_data = Some(data.into());
348 self
349 }
350
351 pub fn run(&mut self) -> std::io::Result<P4Output> {
353 let mut cmd = Command::new(&self.cli.bin_path);
354 cmd.args(&self.args)
355 .stdout(Stdio::piped())
356 .stderr(Stdio::piped());
357
358 if self.stdin_data.is_some() {
359 cmd.stdin(Stdio::piped());
360 } else {
361 cmd.stdin(Stdio::null());
362 }
363
364 if let Some(ref cwd) = self.cwd {
365 cmd.current_dir(cwd);
366 }
367 for (k, v) in &self.envs {
368 cmd.env(k, v);
369 }
370
371 let mut child = cmd.spawn()?;
372
373 if let Some(data) = self.stdin_data.take()
374 && let Some(mut stdin) = child.stdin.take()
375 {
376 thread::spawn(move || {
377 let _ = stdin.write_all(&data);
378 });
379 }
380
381 let stdout = child
382 .stdout
383 .take()
384 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
385 let stderr = child
386 .stderr
387 .take()
388 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
389
390 let stdout_handle = thread::spawn(move || {
391 let mut buf = Vec::new();
392 BufReader::new(stdout).read_to_end(&mut buf)?;
393 Ok::<_, std::io::Error>(buf)
394 });
395
396 let stderr_handle = thread::spawn(move || {
397 let mut buf = Vec::new();
398 BufReader::new(stderr).read_to_end(&mut buf)?;
399 Ok::<_, std::io::Error>(buf)
400 });
401
402 let exit_status = wait_process(&mut child, self.timeout)?;
403
404 let stdout_buf = stdout_handle
405 .join()
406 .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
407 .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
408 let stderr_buf = stderr_handle
409 .join()
410 .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
411 .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
412
413 Ok(P4Output {
414 exit_code: exit_status.code().unwrap_or(-1),
415 stdout: stdout_buf,
416 stderr: stderr_buf,
417 })
418 }
419
420 pub fn stream(&mut self) -> std::io::Result<P4Stream> {
424 let mut cmd = Command::new(&self.cli.bin_path);
425 cmd.args(&self.args)
426 .stdout(Stdio::piped())
427 .stderr(Stdio::piped());
428
429 if self.stdin_data.is_some() {
430 cmd.stdin(Stdio::piped());
431 } else {
432 cmd.stdin(Stdio::null());
433 }
434 if let Some(ref cwd) = self.cwd {
435 cmd.current_dir(cwd);
436 }
437 for (k, v) in &self.envs {
438 cmd.env(k, v);
439 }
440
441 let mut child = cmd.spawn()?;
442
443 if let Some(data) = self.stdin_data.take()
444 && let Some(mut stdin) = child.stdin.take()
445 {
446 thread::spawn(move || {
447 let _ = stdin.write_all(&data);
448 });
449 }
450
451 let mut stdout = child
452 .stdout
453 .take()
454 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
455 let mut stderr = child
456 .stderr
457 .take()
458 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
459
460 let (tx, rx) = std::sync::mpsc::channel();
461 let mut handles = Vec::new();
462
463 let tx_out = tx.clone();
464 handles.push(thread::spawn(move || {
465 let mut buf = vec![0u8; 65536];
466 loop {
467 let n = match stdout.read(&mut buf) {
468 Ok(0) => break,
469 Ok(n) => n,
470 Err(e) => {
471 let _ = tx_out.send(Err(e));
472 break;
473 }
474 };
475 if tx_out
476 .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
477 .is_err()
478 {
479 break;
480 }
481 }
482 }));
483
484 let tx_err = tx.clone();
485 handles.push(thread::spawn(move || {
486 let mut buf = vec![0u8; 65536];
487 loop {
488 let n = match stderr.read(&mut buf) {
489 Ok(0) => break,
490 Ok(n) => n,
491 Err(e) => {
492 let _ = tx_err.send(Err(e));
493 break;
494 }
495 };
496 if tx_err
497 .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
498 .is_err()
499 {
500 break;
501 }
502 }
503 }));
504
505 Ok(P4Stream {
506 rx,
507 child: Some(child),
508 handles,
509 exhausted: false,
510 })
511 }
512}
513
514fn wait_process(child: &mut Child, timeout: Option<Duration>) -> std::io::Result<ExitStatus> {
519 match timeout {
520 None => child.wait(),
521 Some(t) => wait_with_timeout(child, t),
522 }
523}
524
525fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<ExitStatus> {
526 let start = std::time::Instant::now();
527 loop {
528 if let Some(status) = child.try_wait()? {
529 return Ok(status);
530 }
531 if start.elapsed() >= timeout {
532 child.kill()?;
533 return child.wait();
534 }
535 thread::sleep(Duration::from_millis(50));
536 }
537}
538
539impl P4Cli {
544 pub fn new() -> std::io::Result<Self> {
546 let (bin_path, temp_dir) = write_p4_cli_to_disk()?;
547 Ok(Self {
548 bin_path,
549 _temp_dir: temp_dir,
550 })
551 }
552
553 pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
555 self.command().args(args).run()
556 }
557
558 pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
560 self.command().args(args).stream()
561 }
562
563 pub fn command(&self) -> P4Command<'_> {
582 P4Command::new(self)
583 }
584}