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 {
29 exit_code: i32,
30 timed_out: bool,
31 stdout: Vec<u8>,
32 stderr: Vec<u8>,
33}
34
35impl P4Output {
36 pub fn exit_code(&self) -> i32 {
37 self.exit_code
38 }
39
40 pub fn timed_out(&self) -> bool {
42 self.timed_out
43 }
44
45 pub fn success(&self) -> bool {
47 !self.timed_out && self.exit_code == 0
48 }
49
50 pub fn stdout(&self) -> &[u8] {
51 &self.stdout
52 }
53
54 pub fn stderr(&self) -> &[u8] {
55 &self.stderr
56 }
57
58 pub fn stdout_str(&self) -> std::io::Result<&str> {
59 std::str::from_utf8(&self.stdout).map_err(std::io::Error::other)
60 }
61
62 pub fn stderr_str(&self) -> std::io::Result<&str> {
63 std::str::from_utf8(&self.stderr).map_err(std::io::Error::other)
64 }
65
66 pub fn stdout_lines(&self) -> std::io::Result<Vec<&str>> {
67 let s = self.stdout_str()?;
68 if s.is_empty() {
69 Ok(Vec::new())
70 } else {
71 Ok(s.lines().collect())
72 }
73 }
74
75 pub fn stderr_lines(&self) -> std::io::Result<Vec<&str>> {
76 let s = self.stderr_str()?;
77 if s.is_empty() {
78 Ok(Vec::new())
79 } else {
80 Ok(s.lines().collect())
81 }
82 }
83}
84
85impl std::fmt::Debug for P4Output {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("P4Output")
88 .field("exit_code", &self.exit_code)
89 .field("timed_out", &self.timed_out)
90 .field("stdout_len", &self.stdout.len())
91 .field("stderr_len", &self.stderr.len())
92 .finish()
93 }
94}
95
96pub enum P4StreamEvent {
102 Stdout(Vec<u8>),
103 Stderr(Vec<u8>),
104 Exit(i32),
105}
106
107impl P4StreamEvent {
108 pub fn as_utf8(&self) -> Option<&str> {
110 match self {
111 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
112 std::str::from_utf8(data).ok()
113 }
114 P4StreamEvent::Exit(_) => None,
115 }
116 }
117}
118
119impl std::fmt::Display for P4StreamEvent {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
123 if let Ok(text) = std::str::from_utf8(data) {
124 write!(f, "{text}")
125 } else {
126 write!(f, "<{} bytes>", data.len())
127 }
128 }
129 P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
130 }
131 }
132}
133
134pub struct P4Stream {
162 rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
163 child: Option<Child>,
164 exhausted: bool,
165}
166
167impl std::fmt::Debug for P4Stream {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 f.debug_struct("P4Stream")
170 .field("exhausted", &self.exhausted)
171 .finish()
172 }
173}
174
175impl Iterator for P4Stream {
176 type Item = std::io::Result<P4StreamEvent>;
177
178 fn next(&mut self) -> Option<Self::Item> {
179 if self.exhausted {
180 return None;
181 }
182 match self.rx.recv() {
183 Ok(item) => Some(item),
184 Err(_) => {
185 self.exhausted = true;
186 match self.child.take() {
188 Some(mut c) => match c.wait() {
189 Ok(status) => Some(Ok(P4StreamEvent::Exit(status.code().unwrap_or(-1)))),
190 Err(e) => Some(Err(e)),
191 },
192 None => None,
193 }
194 }
195 }
196 }
197}
198
199impl Drop for P4Stream {
200 fn drop(&mut self) {
201 if let Some(ref mut child) = self.child {
206 let _ = child.kill();
207 let _ = child.wait();
208 }
209 }
210}
211
212fn write_p4_cli_to_disk() -> std::io::Result<(PathBuf, tempfile::TempDir)> {
217 let zst_data = get_p4_cli_zst();
218 let binary_data = decompress_zst(&zst_data)?;
219
220 let temp_dir = create_temp_dir()?;
221 let bin_path = temp_dir.path().join("p4_binary");
222 let tmp_path = temp_dir.path().join(".tmp");
223
224 {
225 let mut file = std::fs::File::create(&tmp_path)?;
226 file.write_all(&binary_data)?;
227 file.sync_all()?;
228 }
229 std::fs::rename(&tmp_path, &bin_path)?;
230 set_executable_perms(&bin_path)?;
231
232 Ok((bin_path, temp_dir))
233}
234
235fn create_temp_dir() -> std::io::Result<tempfile::TempDir> {
236 let mut builder = tempfile::Builder::new();
237 builder.prefix("p4cli-20251");
238 #[cfg(unix)]
239 {
240 use std::os::unix::fs::PermissionsExt;
241 builder.permissions(std::fs::Permissions::from_mode(0o700));
242 }
243 builder.tempdir()
244}
245
246fn decompress_zst(zst_data: &[u8]) -> std::io::Result<Vec<u8>> {
247 let mut decoder = zstd::stream::Decoder::new(zst_data)?;
248 let mut buf = Vec::new();
249 std::io::copy(&mut decoder, &mut buf)?;
250 Ok(buf)
251}
252
253#[cfg(unix)]
254fn set_executable_perms(path: &std::path::Path) -> std::io::Result<()> {
255 use std::os::unix::fs::PermissionsExt;
256 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
257}
258
259#[cfg(not(unix))]
260fn set_executable_perms(_path: &std::path::Path) -> std::io::Result<()> {
261 Ok(())
262}
263
264fn get_p4_cli_zst() -> Vec<u8> {
265 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
266 {
267 use p4cli_20251_win_x64::get_p4_cli_zst;
268 get_p4_cli_zst()
269 }
270
271 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
272 {
273 use p4cli_20251_mac_arm64::get_p4_cli_zst;
274 get_p4_cli_zst()
275 }
276
277 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
278 {
279 use p4cli_20251_mac_x64::get_p4_cli_zst;
280 get_p4_cli_zst()
281 }
282
283 #[cfg(all(
284 target_os = "linux",
285 target_arch = "x86_64",
286 any(target_env = "gnu", target_env = "musl")
287 ))]
288 {
289 use p4cli_20251_linux_x64::get_p4_cli_zst;
290 get_p4_cli_zst()
291 }
292
293 #[cfg(all(
294 target_os = "linux",
295 target_arch = "aarch64",
296 any(target_env = "gnu", target_env = "musl")
297 ))]
298 {
299 use p4cli_20251_linux_arm64::get_p4_cli_zst;
300 get_p4_cli_zst()
301 }
302
303 #[cfg(not(any(
304 all(target_os = "windows", target_arch = "x86_64"),
305 all(target_os = "macos", target_arch = "aarch64"),
306 all(target_os = "macos", target_arch = "x86_64"),
307 all(
308 target_os = "linux",
309 target_arch = "x86_64",
310 any(target_env = "gnu", target_env = "musl")
311 ),
312 all(
313 target_os = "linux",
314 target_arch = "aarch64",
315 any(target_env = "gnu", target_env = "musl")
316 ),
317 )))]
318 {
319 compile_error!(format!(
320 "Unsupported platform: {}-{}",
321 std::env::consts::OS,
322 std::env::consts::ARCH
323 ));
324 Vec::new()
325 }
326}
327
328pub struct P4Command<'a> {
334 cli: &'a P4Cli,
335 args: Vec<std::ffi::OsString>,
336 timeout: Option<Duration>,
337 cwd: Option<PathBuf>,
338 envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
339 stdin_data: Option<Vec<u8>>,
340}
341
342impl<'a> P4Command<'a> {
343 fn new(cli: &'a P4Cli) -> Self {
344 Self {
345 cli,
346 args: Vec::new(),
347 timeout: None,
348 cwd: None,
349 envs: Vec::new(),
350 stdin_data: None,
351 }
352 }
353
354 pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
355 self.args.push(arg.as_ref().to_os_string());
356 self
357 }
358
359 pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
360 self.args
361 .extend(args.iter().map(|a| a.as_ref().to_os_string()));
362 self
363 }
364
365 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
370 self.timeout = Some(timeout);
371 self
372 }
373
374 pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
375 self.cwd = Some(path.into());
376 self
377 }
378
379 pub fn env(
380 &mut self,
381 key: impl Into<std::ffi::OsString>,
382 val: impl Into<std::ffi::OsString>,
383 ) -> &mut Self {
384 self.envs.push((key.into(), val.into()));
385 self
386 }
387
388 pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
389 self.stdin_data = Some(data.into());
390 self
391 }
392
393 pub fn run(&mut self) -> std::io::Result<P4Output> {
398 let mut cmd = Command::new(&self.cli.bin_path);
399 cmd.args(&self.args)
400 .stdout(Stdio::piped())
401 .stderr(Stdio::piped());
402
403 if self.stdin_data.is_some() {
404 cmd.stdin(Stdio::piped());
405 } else {
406 cmd.stdin(Stdio::null());
407 }
408
409 if let Some(ref cwd) = self.cwd {
410 cmd.current_dir(cwd);
411 }
412 for (k, v) in &self.envs {
413 cmd.env(k, v);
414 }
415
416 let mut child = cmd.spawn()?;
417
418 let stdin_handle = self.stdin_data.take().and_then(|data| {
419 child.stdin.take().map(|mut stdin| {
420 thread::spawn(move || {
421 if let Err(e) = stdin.write_all(&data) {
422 Err(e)
423 } else {
424 Ok(())
425 }
426 })
427 })
428 });
429
430 let stdout = child
431 .stdout
432 .take()
433 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
434 let stderr = child
435 .stderr
436 .take()
437 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
438
439 let stdout_handle = thread::spawn(move || {
440 let mut buf = Vec::new();
441 BufReader::new(stdout).read_to_end(&mut buf)?;
442 Ok::<_, std::io::Error>(buf)
443 });
444
445 let stderr_handle = thread::spawn(move || {
446 let mut buf = Vec::new();
447 BufReader::new(stderr).read_to_end(&mut buf)?;
448 Ok::<_, std::io::Error>(buf)
449 });
450
451 let (exit_status, timed_out) = wait_process(&mut child, self.timeout)?;
452
453 if let Some(handle) = stdin_handle {
455 handle
456 .join()
457 .map_err(|_| std::io::Error::other("stdin thread panicked"))?
458 .map_err(|e| std::io::Error::other(format!("stdin write failed: {e}")))?;
459 }
460
461 let stdout_buf = stdout_handle
462 .join()
463 .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
464 .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
465 let stderr_buf = stderr_handle
466 .join()
467 .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
468 .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
469
470 Ok(P4Output {
471 exit_code: exit_status.code().unwrap_or(-1),
472 timed_out,
473 stdout: stdout_buf,
474 stderr: stderr_buf,
475 })
476 }
477
478 pub fn stream(&mut self) -> std::io::Result<P4Stream> {
486 if self.timeout.is_some() {
487 return Err(std::io::Error::other(
488 "timeout is not supported on stream(); use run() instead",
489 ));
490 }
491
492 let mut cmd = Command::new(&self.cli.bin_path);
493 cmd.args(&self.args)
494 .stdout(Stdio::piped())
495 .stderr(Stdio::piped());
496
497 if self.stdin_data.is_some() {
498 cmd.stdin(Stdio::piped());
499 } else {
500 cmd.stdin(Stdio::null());
501 }
502 if let Some(ref cwd) = self.cwd {
503 cmd.current_dir(cwd);
504 }
505 for (k, v) in &self.envs {
506 cmd.env(k, v);
507 }
508
509 let mut child = cmd.spawn()?;
510
511 if let Some(data) = self.stdin_data.take()
512 && let Some(mut stdin) = child.stdin.take()
513 {
514 thread::spawn(move || {
515 let _ = stdin.write_all(&data);
516 });
517 }
518
519 let mut stdout = child
520 .stdout
521 .take()
522 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
523 let mut stderr = child
524 .stderr
525 .take()
526 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
527
528 let (tx, rx) = std::sync::mpsc::sync_channel(64);
530
531 let tx_out = tx.clone();
532 thread::spawn(move || {
533 let mut buf = vec![0u8; 65536];
534 loop {
535 let n = match stdout.read(&mut buf) {
536 Ok(0) => break,
537 Ok(n) => n,
538 Err(e) => {
539 let _ = tx_out.send(Err(e));
540 break;
541 }
542 };
543 if tx_out
544 .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
545 .is_err()
546 {
547 break;
548 }
549 }
550 });
551
552 let tx_err = tx.clone();
553 thread::spawn(move || {
554 let mut buf = vec![0u8; 65536];
555 loop {
556 let n = match stderr.read(&mut buf) {
557 Ok(0) => break,
558 Ok(n) => n,
559 Err(e) => {
560 let _ = tx_err.send(Err(e));
561 break;
562 }
563 };
564 if tx_err
565 .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
566 .is_err()
567 {
568 break;
569 }
570 }
571 });
572
573 Ok(P4Stream {
574 rx,
575 child: Some(child),
576 exhausted: false,
577 })
578 }
579}
580
581fn wait_process(
586 child: &mut Child,
587 timeout: Option<Duration>,
588) -> std::io::Result<(ExitStatus, bool)> {
589 match timeout {
590 None => Ok((child.wait()?, false)),
591 Some(t) => wait_with_timeout(child, t),
592 }
593}
594
595fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<(ExitStatus, bool)> {
596 let start = std::time::Instant::now();
597 loop {
598 if let Some(status) = child.try_wait()? {
599 return Ok((status, false));
600 }
601 if start.elapsed() >= timeout {
602 child.kill()?;
603 return Ok((child.wait()?, true));
604 }
605 thread::sleep(Duration::from_millis(50));
606 }
607}
608
609impl P4Cli {
614 pub fn new() -> std::io::Result<Self> {
616 let (bin_path, temp_dir) = write_p4_cli_to_disk()?;
617 Ok(Self {
618 bin_path,
619 _temp_dir: temp_dir,
620 })
621 }
622
623 pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
625 self.command().args(args).run()
626 }
627
628 pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
630 self.command().args(args).stream()
631 }
632
633 pub fn command(&self) -> P4Command<'_> {
652 P4Command::new(self)
653 }
654}