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 {
27 bin_path: PathBuf,
28 _temp_dir: PathBuf,
29}
30
31pub struct P4Output {
36 exit_code: i32,
37 stdout: Vec<u8>,
38 stderr: Vec<u8>,
39}
40
41impl P4Output {
42 pub fn exit_code(&self) -> i32 {
43 self.exit_code
44 }
45
46 pub fn success(&self) -> bool {
48 self.exit_code == 0
49 }
50
51 pub fn stdout(&self) -> &[u8] {
53 &self.stdout
54 }
55
56 pub fn stderr(&self) -> &[u8] {
58 &self.stderr
59 }
60
61 pub fn stdout_str(&self) -> std::io::Result<&str> {
63 std::str::from_utf8(&self.stdout).map_err(std::io::Error::other)
64 }
65
66 pub fn stderr_str(&self) -> std::io::Result<&str> {
68 std::str::from_utf8(&self.stderr).map_err(std::io::Error::other)
69 }
70
71 pub fn stdout_lines(&self) -> std::io::Result<Vec<&str>> {
73 let s = self.stdout_str()?;
74 if s.is_empty() {
75 Ok(Vec::new())
76 } else {
77 Ok(s.lines().collect())
78 }
79 }
80
81 pub fn stderr_lines(&self) -> std::io::Result<Vec<&str>> {
83 let s = self.stderr_str()?;
84 if s.is_empty() {
85 Ok(Vec::new())
86 } else {
87 Ok(s.lines().collect())
88 }
89 }
90}
91
92impl std::fmt::Debug for P4Output {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("P4Output")
95 .field("exit_code", &self.exit_code)
96 .field("stdout_len", &self.stdout.len())
97 .field("stderr_len", &self.stderr.len())
98 .finish()
99 }
100}
101
102pub enum P4StreamEvent {
108 Stdout(Vec<u8>),
110 Stderr(Vec<u8>),
112 Exit(i32),
114}
115
116impl P4StreamEvent {
117 pub fn as_utf8(&self) -> Option<&str> {
120 match self {
121 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
122 std::str::from_utf8(data).ok()
123 }
124 P4StreamEvent::Exit(_) => None,
125 }
126 }
127}
128
129impl std::fmt::Display for P4StreamEvent {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
133 if let Ok(text) = std::str::from_utf8(data) {
134 write!(f, "{text}")
135 } else {
136 write!(f, "<{} bytes>", data.len())
137 }
138 }
139 P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
140 }
141 }
142}
143
144pub struct P4Stream {
186 rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
187 child: Option<Child>,
188 #[allow(dead_code)]
191 handles: Vec<thread::JoinHandle<()>>,
192 exhausted: bool,
193}
194
195impl Iterator for P4Stream {
196 type Item = std::io::Result<P4StreamEvent>;
197
198 fn next(&mut self) -> Option<Self::Item> {
199 if self.exhausted {
200 return None;
201 }
202 match self.rx.recv() {
203 Ok(item) => Some(item),
204 Err(_) => {
205 self.exhausted = true;
207 let code = self
208 .child
209 .take()
210 .and_then(|mut c| c.wait().ok())
211 .and_then(|s| s.code())
212 .unwrap_or(-1);
213 Some(Ok(P4StreamEvent::Exit(code)))
214 }
215 }
216 }
217}
218
219impl Drop for P4Stream {
220 fn drop(&mut self) {
221 if let Some(ref mut child) = self.child {
223 let _ = child.kill();
224 let _ = child.wait();
225 }
226 }
228}
229
230fn write_p4_cli_to_disk() -> std::io::Result<(PathBuf, PathBuf)> {
237 let zst_data = get_p4_cli_zst();
238 let binary_data = decompress_zst(&zst_data)?;
239
240 let base = std::env::temp_dir().join("p4cli-20251");
241 std::fs::create_dir_all(&base)?;
242
243 let dir = base.join(format!(
244 "{}_{}",
245 std::process::id(),
246 std::time::SystemTime::now()
247 .duration_since(std::time::UNIX_EPOCH)
248 .map(|d| d.as_nanos())
249 .unwrap_or(0)
250 ));
251 std::fs::create_dir(&dir)?;
252
253 let bin_path = dir.join("p4_binary");
254 let tmp_path = dir.join(".tmp");
255
256 {
258 let mut file = std::fs::File::create(&tmp_path)?;
259 file.write_all(&binary_data)?;
260 file.sync_all()?;
261 }
262 std::fs::rename(&tmp_path, &bin_path)?;
263 set_executable_perms(&bin_path)?;
264
265 Ok((bin_path, dir))
266}
267
268fn decompress_zst(zst_data: &[u8]) -> std::io::Result<Vec<u8>> {
269 let mut decoder = zstd::stream::Decoder::new(zst_data)?;
270 let mut buf = Vec::new();
271 std::io::copy(&mut decoder, &mut buf)?;
272 Ok(buf)
273}
274
275#[cfg(unix)]
276fn set_executable_perms(path: &std::path::Path) -> std::io::Result<()> {
277 use std::os::unix::fs::PermissionsExt;
278 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
279}
280
281#[cfg(not(unix))]
282fn set_executable_perms(_path: &std::path::Path) -> std::io::Result<()> {
283 Ok(())
284}
285
286fn get_p4_cli_zst() -> Vec<u8> {
291 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
292 {
293 use p4cli_20251_win_x64::get_p4_cli_zst;
294 get_p4_cli_zst()
295 }
296
297 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
298 {
299 use p4cli_20251_mac_arm64::get_p4_cli_zst;
300 get_p4_cli_zst()
301 }
302
303 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
304 {
305 use p4cli_20251_mac_x64::get_p4_cli_zst;
306 get_p4_cli_zst()
307 }
308
309 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
310 {
311 use p4cli_20251_linux_x64::get_p4_cli_zst;
312 get_p4_cli_zst()
313 }
314
315 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
316 {
317 use p4cli_20251_linux_arm64::get_p4_cli_zst;
318 get_p4_cli_zst()
319 }
320
321 #[cfg(not(any(
322 all(target_os = "windows", target_arch = "x86_64"),
323 all(target_os = "macos", target_arch = "aarch64"),
324 all(target_os = "macos", target_arch = "x86_64"),
325 all(target_os = "linux", target_arch = "x86_64"),
326 all(target_os = "linux", target_arch = "aarch64")
327 )))]
328 {
329 compile_error!(format!(
330 "Unsupported platform: {}-{}",
331 std::env::consts::OS,
332 std::env::consts::ARCH
333 ));
334 Vec::new()
335 }
336}
337
338pub struct P4Command<'a> {
347 cli: &'a P4Cli,
348 args: Vec<std::ffi::OsString>,
349 timeout: Option<Duration>,
350 cwd: Option<PathBuf>,
351 envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
352 stdin_data: Option<Vec<u8>>,
353}
354
355impl<'a> P4Command<'a> {
356 fn new(cli: &'a P4Cli) -> Self {
357 Self {
358 cli,
359 args: Vec::new(),
360 timeout: None,
361 cwd: None,
362 envs: Vec::new(),
363 stdin_data: None,
364 }
365 }
366
367 pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
369 self.args.push(arg.as_ref().to_os_string());
370 self
371 }
372
373 pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
375 self.args
376 .extend(args.iter().map(|a| a.as_ref().to_os_string()));
377 self
378 }
379
380 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
386 self.timeout = Some(timeout);
387 self
388 }
389
390 pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
392 self.cwd = Some(path.into());
393 self
394 }
395
396 pub fn env(
398 &mut self,
399 key: impl Into<std::ffi::OsString>,
400 val: impl Into<std::ffi::OsString>,
401 ) -> &mut Self {
402 self.envs.push((key.into(), val.into()));
403 self
404 }
405
406 pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
408 self.stdin_data = Some(data.into());
409 self
410 }
411
412 pub fn run(&mut self) -> std::io::Result<P4Output> {
418 let mut cmd = Command::new(&self.cli.bin_path);
419 cmd.args(&self.args)
420 .stdout(Stdio::piped())
421 .stderr(Stdio::piped());
422
423 if self.stdin_data.is_some() {
424 cmd.stdin(Stdio::piped());
425 } else {
426 cmd.stdin(Stdio::null());
427 }
428
429 if let Some(ref cwd) = self.cwd {
430 cmd.current_dir(cwd);
431 }
432 for (k, v) in &self.envs {
433 cmd.env(k, v);
434 }
435
436 let mut child = cmd.spawn()?;
437
438 if let Some(data) = self.stdin_data.take()
440 && let Some(mut stdin) = child.stdin.take()
441 {
442 thread::spawn(move || {
443 let _ = stdin.write_all(&data);
444 });
445 }
446
447 let stdout = child
448 .stdout
449 .take()
450 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
451 let stderr = child
452 .stderr
453 .take()
454 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
455
456 let stdout_handle = thread::spawn(move || {
458 let mut buf = Vec::new();
459 BufReader::new(stdout).read_to_end(&mut buf)?;
460 Ok::<_, std::io::Error>(buf)
461 });
462
463 let stderr_handle = thread::spawn(move || {
464 let mut buf = Vec::new();
465 BufReader::new(stderr).read_to_end(&mut buf)?;
466 Ok::<_, std::io::Error>(buf)
467 });
468
469 let exit_status = wait_process(&mut child, self.timeout)?;
471
472 let stdout_buf = stdout_handle
473 .join()
474 .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
475 .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
476 let stderr_buf = stderr_handle
477 .join()
478 .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
479 .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
480
481 Ok(P4Output {
482 exit_code: exit_status.code().unwrap_or(-1),
483 stdout: stdout_buf,
484 stderr: stderr_buf,
485 })
486 }
487
488 pub fn stream(&mut self) -> std::io::Result<P4Stream> {
499 let mut cmd = Command::new(&self.cli.bin_path);
500 cmd.args(&self.args)
501 .stdout(Stdio::piped())
502 .stderr(Stdio::piped());
503
504 if self.stdin_data.is_some() {
505 cmd.stdin(Stdio::piped());
506 } else {
507 cmd.stdin(Stdio::null());
508 }
509 if let Some(ref cwd) = self.cwd {
510 cmd.current_dir(cwd);
511 }
512 for (k, v) in &self.envs {
513 cmd.env(k, v);
514 }
515
516 let mut child = cmd.spawn()?;
517
518 if let Some(data) = self.stdin_data.take()
519 && let Some(mut stdin) = child.stdin.take()
520 {
521 thread::spawn(move || {
522 let _ = stdin.write_all(&data);
523 });
524 }
525
526 let mut stdout = child
527 .stdout
528 .take()
529 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
530 let mut stderr = child
531 .stderr
532 .take()
533 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
534
535 let (tx, rx) = std::sync::mpsc::channel();
536 let mut handles = Vec::new();
537
538 let tx_out = tx.clone();
540 handles.push(thread::spawn(move || {
541 let mut buf = vec![0u8; 65536];
542 loop {
543 let n = match stdout.read(&mut buf) {
544 Ok(0) => break, Ok(n) => n,
546 Err(e) => {
547 let _ = tx_out.send(Err(e));
548 break;
549 }
550 };
551 if tx_out
552 .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
553 .is_err()
554 {
555 break;
556 }
557 }
558 }));
559
560 let tx_err = tx.clone();
562 handles.push(thread::spawn(move || {
563 let mut buf = vec![0u8; 65536];
564 loop {
565 let n = match stderr.read(&mut buf) {
566 Ok(0) => break,
567 Ok(n) => n,
568 Err(e) => {
569 let _ = tx_err.send(Err(e));
570 break;
571 }
572 };
573 if tx_err
574 .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
575 .is_err()
576 {
577 break;
578 }
579 }
580 }));
581
582 Ok(P4Stream {
583 rx,
584 child: Some(child),
585 handles,
586 exhausted: false,
587 })
588 }
589}
590
591fn wait_process(child: &mut Child, timeout: Option<Duration>) -> std::io::Result<ExitStatus> {
593 match timeout {
594 None => child.wait(),
595 Some(t) => wait_with_timeout(child, t),
596 }
597}
598
599fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<ExitStatus> {
600 let start = std::time::Instant::now();
601 loop {
602 if let Some(status) = child.try_wait()? {
603 return Ok(status);
604 }
605 if start.elapsed() >= timeout {
606 child.kill()?;
607 return child.wait();
608 }
609 thread::sleep(Duration::from_millis(50));
610 }
611}
612
613impl P4Cli {
618 pub fn new() -> std::io::Result<Self> {
624 let (bin_path, temp_dir) = write_p4_cli_to_disk()?;
625 Ok(Self {
626 bin_path,
627 _temp_dir: temp_dir,
628 })
629 }
630
631 pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
635 self.command().args(args).run()
636 }
637
638 pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
642 self.command().args(args).stream()
643 }
644
645 pub fn command(&self) -> P4Command<'_> {
669 P4Command::new(self)
670 }
671}
672
673impl Drop for P4Cli {
674 fn drop(&mut self) {
675 let _ = std::fs::remove_dir_all(&self._temp_dir);
676 }
677}
678
679#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_run_help() -> std::io::Result<()> {
689 let p4 = P4Cli::new()?;
690 let output = p4.run(&["--help"])?;
691 assert!(output.success(), "p4 --help should exit with 0");
692 let stdout = output.stdout_str()?;
693 assert!(
694 stdout.contains("Usage:"),
695 "expected --help output to contain 'Usage:'"
696 );
697 Ok(())
698 }
699
700 #[test]
701 fn test_run_error() -> std::io::Result<()> {
702 let p4 = P4Cli::new()?;
703 let output = p4.run(&["--nonexistent-flag"])?;
704 assert!(!output.success(), "unknown flag should exit non-zero");
705 let stderr = output.stderr_str()?;
706 assert!(
707 stderr.contains("Invalid option") || stderr.contains("error"),
708 "expected error output, got: {stderr}"
709 );
710 Ok(())
711 }
712
713 #[test]
714 fn test_multiple_instances() -> std::io::Result<()> {
715 let p4_a = P4Cli::new()?;
716 let p4_b = P4Cli::new()?;
717 assert!(p4_a.run(&["--help"])?.success());
718 assert!(p4_b.run(&["--help"])?.success());
719 Ok(())
720 }
721
722 #[test]
723 fn test_command_builder() -> std::io::Result<()> {
724 let p4 = P4Cli::new()?;
725 let output = p4.command().arg("--help").run()?;
726 assert!(output.success());
727 Ok(())
728 }
729
730 #[test]
731 fn test_timeout_kills() -> std::io::Result<()> {
732 let p4 = P4Cli::new()?;
733 let output = p4
735 .command()
736 .arg("help")
737 .timeout(Duration::from_millis(1))
738 .run()?;
739 assert!(!output.success() || output.exit_code() == 0);
742 Ok(())
743 }
744
745 #[test]
746 fn test_stream_help() -> std::io::Result<()> {
747 let p4 = P4Cli::new()?;
748 let mut saw_stdout = false;
749 let mut saw_exit = false;
750 for event in p4.stream(&["--help"])? {
751 match &event? {
752 P4StreamEvent::Stdout(data) => {
753 if let Ok(text) = std::str::from_utf8(data)
754 && text.contains("Usage:")
755 {
756 saw_stdout = true;
757 }
758 }
759 P4StreamEvent::Stderr(_) => {}
760 P4StreamEvent::Exit(code) => {
761 assert_eq!(*code, 0);
762 saw_exit = true;
763 }
764 }
765 }
766 assert!(saw_stdout, "expected --help to contain 'Usage:'");
767 assert!(saw_exit, "expected Exit event");
768 Ok(())
769 }
770
771 #[test]
772 fn test_stream_error() -> std::io::Result<()> {
773 let p4 = P4Cli::new()?;
774 let mut saw_stderr = false;
775 let mut saw_exit = false;
776 for event in p4.stream(&["--nonexistent-flag"])? {
777 match &event? {
778 P4StreamEvent::Stdout(_) => {}
779 P4StreamEvent::Stderr(data) => {
780 if let Ok(text) = std::str::from_utf8(data)
781 && (text.contains("Invalid option") || text.contains("error"))
782 {
783 saw_stderr = true;
784 }
785 }
786 P4StreamEvent::Exit(code) => {
787 assert_ne!(*code, 0, "nonexistent flag should fail");
788 saw_exit = true;
789 }
790 }
791 }
792 assert!(saw_stderr, "expected error output");
793 assert!(saw_exit, "expected Exit event");
794 Ok(())
795 }
796
797 #[test]
798 fn test_stream_drop_midway() -> std::io::Result<()> {
799 let p4 = P4Cli::new()?;
801 let stream = p4.stream(&["--help"])?;
802 drop(stream);
803 Ok(())
804 }
805
806 #[test]
807 fn test_stream_builder() -> std::io::Result<()> {
808 let p4 = P4Cli::new()?;
809 let mut saw_exit = false;
810 for event in p4.command().arg("--help").stream()? {
811 if let P4StreamEvent::Exit(code) = event? {
812 assert_eq!(code, 0);
813 saw_exit = true;
814 }
815 }
816 assert!(saw_exit);
817 Ok(())
818 }
819}