1use std::{
2 env,
3 error::Error,
4 fmt,
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7 process::{Command, Stdio},
8 thread,
9};
10
11use serde::{Deserialize, Serialize};
12
13const LOCAL_NETWORK: &str = "local";
14pub(crate) const CANIC_ICP_LOCAL_NETWORK_URL_ENV: &str = "CANIC_ICP_LOCAL_NETWORK_URL";
15pub(crate) const CANIC_ICP_LOCAL_ROOT_KEY_ENV: &str = "CANIC_ICP_LOCAL_ROOT_KEY";
16
17#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct IcpRawOutput {
23 pub success: bool,
24 pub status: String,
25 pub stdout: Vec<u8>,
26 pub stderr: Vec<u8>,
27}
28
29#[derive(Debug)]
34pub enum IcpCommandError {
35 Io(std::io::Error),
36 Failed {
37 command: String,
38 stderr: String,
39 },
40 Json {
41 command: String,
42 output: String,
43 source: serde_json::Error,
44 },
45 SnapshotIdUnavailable {
46 output: String,
47 },
48}
49
50impl fmt::Display for IcpCommandError {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Io(err) => write!(formatter, "{err}"),
55 Self::Failed { command, stderr } => {
56 write!(formatter, "icp command failed: {command}\n{stderr}")
57 }
58 Self::Json {
59 command,
60 output,
61 source,
62 } => {
63 write!(
64 formatter,
65 "could not parse icp json output for {command}: {source}\n{output}"
66 )
67 }
68 Self::SnapshotIdUnavailable { output } => {
69 write!(
70 formatter,
71 "could not parse snapshot id from icp output: {output}"
72 )
73 }
74 }
75 }
76}
77
78impl Error for IcpCommandError {
79 fn source(&self) -> Option<&(dyn Error + 'static)> {
81 match self {
82 Self::Io(err) => Some(err),
83 Self::Json { source, .. } => Some(source),
84 Self::Failed { .. } | Self::SnapshotIdUnavailable { .. } => None,
85 }
86 }
87}
88
89impl From<std::io::Error> for IcpCommandError {
90 fn from(err: std::io::Error) -> Self {
92 Self::Io(err)
93 }
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct IcpCli {
102 executable: String,
103 environment: Option<String>,
104 network: Option<String>,
105 cwd: Option<PathBuf>,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
113pub struct IcpSnapshotCreateReceipt {
114 pub snapshot_id: String,
115 pub taken_at_timestamp: Option<u64>,
116 pub total_size_bytes: Option<u64>,
117}
118
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
124pub struct IcpSnapshotUploadReceipt {
125 pub snapshot_id: String,
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
133pub struct IcpCanisterStatusReport {
134 pub id: String,
135 pub name: Option<String>,
136 pub status: String,
137 pub settings: Option<IcpCanisterStatusSettings>,
138 pub module_hash: Option<String>,
139 pub memory_size: Option<String>,
140 pub cycles: Option<String>,
141 pub reserved_cycles: Option<String>,
142 pub idle_cycles_burned_per_day: Option<String>,
143}
144
145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150pub struct IcpCanisterStatusSettings {
151 #[serde(default)]
152 pub controllers: Vec<String>,
153 pub compute_allocation: Option<String>,
154 pub memory_allocation: Option<String>,
155 pub freezing_threshold: Option<String>,
156 pub reserved_cycles_limit: Option<String>,
157 pub wasm_memory_limit: Option<String>,
158 pub wasm_memory_threshold: Option<String>,
159 pub log_memory_limit: Option<String>,
160}
161
162impl IcpCli {
163 #[must_use]
165 pub fn new(
166 executable: impl Into<String>,
167 environment: Option<String>,
168 network: Option<String>,
169 ) -> Self {
170 Self {
171 executable: executable.into(),
172 environment,
173 network,
174 cwd: None,
175 }
176 }
177
178 #[must_use]
180 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
181 self.cwd = Some(cwd.into());
182 self
183 }
184
185 #[must_use]
187 pub fn environment(&self) -> Option<&str> {
188 self.environment.as_deref()
189 }
190
191 #[must_use]
193 pub fn network(&self) -> Option<&str> {
194 self.network.as_deref()
195 }
196
197 #[must_use]
199 pub fn command(&self) -> Command {
200 let mut command = Command::new(&self.executable);
201 if let Some(cwd) = &self.cwd {
202 command.current_dir(cwd);
203 add_project_root_override_arg(&mut command, cwd);
204 }
205 command
206 }
207
208 #[must_use]
210 pub fn command_in(&self, cwd: &Path) -> Command {
211 let mut command = Command::new(&self.executable);
212 command.current_dir(cwd);
213 add_project_root_override_arg(&mut command, cwd);
214 command
215 }
216
217 #[must_use]
219 pub fn canister_command(&self) -> Command {
220 let mut command = self.command();
221 command.arg("canister");
222 command
223 }
224
225 pub fn version(&self) -> Result<String, IcpCommandError> {
227 let mut command = self.command();
228 command.arg("--version");
229 run_output(&mut command)
230 }
231
232 pub fn local_replica_start(
234 &self,
235 background: bool,
236 debug: bool,
237 ) -> Result<String, IcpCommandError> {
238 let mut command = self.local_replica_command("start");
239 run_local_replica_start_command(&mut command, background, debug)
240 }
241
242 pub fn local_replica_start_in(
244 &self,
245 cwd: &Path,
246 background: bool,
247 debug: bool,
248 ) -> Result<String, IcpCommandError> {
249 let mut command = self.local_replica_command_in("start", cwd);
250 run_local_replica_start_command(&mut command, background, debug)
251 }
252
253 pub fn local_replica_status(&self, debug: bool) -> Result<String, IcpCommandError> {
255 let mut command = self.local_replica_command("status");
256 add_debug_arg(&mut command, debug);
257 run_output_with_stderr(&mut command)
258 }
259
260 pub fn local_replica_status_in(
262 &self,
263 cwd: &Path,
264 debug: bool,
265 ) -> Result<String, IcpCommandError> {
266 let mut command = self.local_replica_command_in("status", cwd);
267 add_debug_arg(&mut command, debug);
268 run_output_with_stderr(&mut command)
269 }
270
271 pub fn local_replica_status_json(
273 &self,
274 debug: bool,
275 ) -> Result<serde_json::Value, IcpCommandError> {
276 let mut command = self.local_replica_command("status");
277 add_debug_arg(&mut command, debug);
278 command.arg("--json");
279 run_json(&mut command)
280 }
281
282 pub fn local_replica_status_json_in(
284 &self,
285 cwd: &Path,
286 debug: bool,
287 ) -> Result<serde_json::Value, IcpCommandError> {
288 let mut command = self.local_replica_command_in("status", cwd);
289 add_debug_arg(&mut command, debug);
290 command.arg("--json");
291 run_json(&mut command)
292 }
293
294 pub fn local_replica_project_running(&self, debug: bool) -> Result<bool, IcpCommandError> {
296 let mut command = self.local_replica_command("status");
297 add_debug_arg(&mut command, debug);
298 run_success(&mut command)
299 }
300
301 pub fn local_replica_project_running_in(
303 &self,
304 cwd: &Path,
305 debug: bool,
306 ) -> Result<bool, IcpCommandError> {
307 let mut command = self.local_replica_command_in("status", cwd);
308 add_debug_arg(&mut command, debug);
309 run_success(&mut command)
310 }
311
312 pub fn local_replica_ping(&self, debug: bool) -> Result<bool, IcpCommandError> {
314 let mut command = self.local_replica_command("ping");
315 add_debug_arg(&mut command, debug);
316 run_success(&mut command)
317 }
318
319 pub fn local_replica_stop(&self, debug: bool) -> Result<String, IcpCommandError> {
321 let mut command = self.local_replica_command("stop");
322 add_debug_arg(&mut command, debug);
323 run_output_with_stderr(&mut command)
324 }
325
326 pub fn local_replica_stop_in(
328 &self,
329 cwd: &Path,
330 debug: bool,
331 ) -> Result<String, IcpCommandError> {
332 let mut command = self.local_replica_command_in("stop", cwd);
333 add_debug_arg(&mut command, debug);
334 run_output_with_stderr(&mut command)
335 }
336
337 #[must_use]
339 pub fn local_replica_start_display(&self, background: bool, debug: bool) -> String {
340 let mut command = self.local_replica_command("start");
341 add_debug_arg(&mut command, debug);
342 if background {
343 command.arg("--background");
344 }
345 command_display(&command)
346 }
347
348 #[must_use]
350 pub fn local_replica_status_display(&self, debug: bool) -> String {
351 let mut command = self.local_replica_command("status");
352 add_debug_arg(&mut command, debug);
353 command_display(&command)
354 }
355
356 #[must_use]
358 pub fn local_replica_stop_display(&self, debug: bool) -> String {
359 let mut command = self.local_replica_command("stop");
360 add_debug_arg(&mut command, debug);
361 command_display(&command)
362 }
363
364 fn local_replica_command(&self, action: &str) -> Command {
365 let mut command = self.command();
366 command.args(["network", action]);
367 self.add_local_network_target(&mut command);
368 command
369 }
370
371 fn local_replica_command_in(&self, action: &str, cwd: &Path) -> Command {
372 let mut command = self.command_in(cwd);
373 command.args(["network", action]);
374 self.add_local_network_target(&mut command);
375 command
376 }
377
378 pub fn canister_call_output(
380 &self,
381 canister: &str,
382 method: &str,
383 output: Option<&str>,
384 ) -> Result<String, IcpCommandError> {
385 let mut command = self.canister_command();
386 command.args(["call", canister, method]);
387 command.arg("()");
388 if let Some(output) = output {
389 add_output_arg(&mut command, output);
390 }
391 self.add_target_args(&mut command);
392 run_output(&mut command)
393 }
394
395 pub fn canister_call_arg_output(
397 &self,
398 canister: &str,
399 method: &str,
400 arg: &str,
401 output: Option<&str>,
402 ) -> Result<String, IcpCommandError> {
403 let mut command = self.canister_command();
404 command.args(["call", canister, method]);
405 command.arg(arg);
406 if let Some(output) = output {
407 add_output_arg(&mut command, output);
408 }
409 self.add_target_args(&mut command);
410 run_output(&mut command)
411 }
412
413 pub fn canister_query_output(
415 &self,
416 canister: &str,
417 method: &str,
418 output: Option<&str>,
419 ) -> Result<String, IcpCommandError> {
420 let mut command = self.canister_command();
421 command.args(["call", canister, method]);
422 command.arg("()");
423 command.arg("--query");
424 if let Some(output) = output {
425 add_output_arg(&mut command, output);
426 }
427 self.add_target_args(&mut command);
428 run_output(&mut command)
429 }
430
431 pub fn canister_query_arg_output(
433 &self,
434 canister: &str,
435 method: &str,
436 arg: &str,
437 output: Option<&str>,
438 ) -> Result<String, IcpCommandError> {
439 let mut command = self.canister_command();
440 command.args(["call", canister, method]);
441 command.arg(arg);
442 command.arg("--query");
443 if let Some(output) = output {
444 add_output_arg(&mut command, output);
445 }
446 self.add_target_args(&mut command);
447 run_output(&mut command)
448 }
449
450 pub fn canister_metadata_output(
452 &self,
453 canister: &str,
454 metadata_name: &str,
455 ) -> Result<String, IcpCommandError> {
456 let mut command = self.canister_command();
457 command.args(["metadata", canister, metadata_name]);
458 self.add_target_args(&mut command);
459 run_output(&mut command)
460 }
461
462 pub fn canister_status(&self, canister: &str) -> Result<String, IcpCommandError> {
464 let mut command = self.canister_command();
465 command.args(["status", canister]);
466 self.add_target_args(&mut command);
467 run_output(&mut command)
468 }
469
470 pub fn canister_top_up_output(
472 &self,
473 canister: &str,
474 amount_cycles: u128,
475 ) -> Result<String, IcpCommandError> {
476 let mut command = self.canister_command();
477 command.args(["top-up", "--amount"]);
478 command.arg(amount_cycles.to_string());
479 command.arg(canister);
480 self.add_target_args(&mut command);
481 run_output_with_stderr(&mut command)
482 }
483
484 pub fn canister_status_report(
486 &self,
487 canister: &str,
488 ) -> Result<IcpCanisterStatusReport, IcpCommandError> {
489 let mut command = self.canister_command();
490 command.args(["status", canister]);
491 command.arg("--json");
492 self.add_target_args(&mut command);
493 run_json(&mut command)
494 }
495
496 pub fn snapshot_create_receipt(
498 &self,
499 canister: &str,
500 ) -> Result<IcpSnapshotCreateReceipt, IcpCommandError> {
501 let mut command = self.canister_command();
502 command.args(["snapshot", "create", canister]);
503 command.arg("--json");
504 self.add_target_args(&mut command);
505 run_json(&mut command)
506 }
507
508 pub fn snapshot_create_id(&self, canister: &str) -> Result<String, IcpCommandError> {
510 Ok(self.snapshot_create_receipt(canister)?.snapshot_id)
511 }
512
513 pub fn stop_canister(&self, canister: &str) -> Result<(), IcpCommandError> {
515 let mut command = self.canister_command();
516 command.args(["stop", canister]);
517 self.add_target_args(&mut command);
518 run_status(&mut command)
519 }
520
521 pub fn start_canister(&self, canister: &str) -> Result<(), IcpCommandError> {
523 let mut command = self.canister_command();
524 command.args(["start", canister]);
525 self.add_target_args(&mut command);
526 run_status(&mut command)
527 }
528
529 pub fn snapshot_download(
531 &self,
532 canister: &str,
533 snapshot_id: &str,
534 artifact_path: &Path,
535 ) -> Result<(), IcpCommandError> {
536 let mut command = self.canister_command();
537 command.args(["snapshot", "download", canister, snapshot_id, "--output"]);
538 command.arg(artifact_path);
539 self.add_target_args(&mut command);
540 run_status(&mut command)
541 }
542
543 pub fn snapshot_upload(
545 &self,
546 canister: &str,
547 artifact_path: &Path,
548 ) -> Result<String, IcpCommandError> {
549 Ok(self
550 .snapshot_upload_receipt(canister, artifact_path)?
551 .snapshot_id)
552 }
553
554 pub fn snapshot_upload_receipt(
556 &self,
557 canister: &str,
558 artifact_path: &Path,
559 ) -> Result<IcpSnapshotUploadReceipt, IcpCommandError> {
560 let mut command = self.canister_command();
561 command.args(["snapshot", "upload", canister, "--input"]);
562 command.arg(artifact_path);
563 command.arg("--resume");
564 command.arg("--json");
565 self.add_target_args(&mut command);
566 run_json(&mut command)
567 }
568
569 pub fn snapshot_restore(
571 &self,
572 canister: &str,
573 snapshot_id: &str,
574 ) -> Result<(), IcpCommandError> {
575 let mut command = self.canister_command();
576 command.args(["snapshot", "restore", canister, snapshot_id]);
577 self.add_target_args(&mut command);
578 run_status(&mut command)
579 }
580
581 #[must_use]
583 pub fn snapshot_create_display(&self, canister: &str) -> String {
584 let mut command = self.canister_command();
585 command.args(["snapshot", "create", canister]);
586 command.arg("--json");
587 self.add_target_args(&mut command);
588 command_display(&command)
589 }
590
591 #[must_use]
593 pub fn snapshot_download_display(
594 &self,
595 canister: &str,
596 snapshot_id: &str,
597 artifact_path: &Path,
598 ) -> String {
599 let mut command = self.canister_command();
600 command.args(["snapshot", "download", canister, snapshot_id, "--output"]);
601 command.arg(artifact_path);
602 self.add_target_args(&mut command);
603 command_display(&command)
604 }
605
606 #[must_use]
608 pub fn snapshot_upload_display(&self, canister: &str, artifact_path: &Path) -> String {
609 let mut command = self.canister_command();
610 command.args(["snapshot", "upload", canister, "--input"]);
611 command.arg(artifact_path);
612 command.arg("--resume");
613 command.arg("--json");
614 self.add_target_args(&mut command);
615 command_display(&command)
616 }
617
618 #[must_use]
620 pub fn snapshot_restore_display(&self, canister: &str, snapshot_id: &str) -> String {
621 let mut command = self.canister_command();
622 command.args(["snapshot", "restore", canister, snapshot_id]);
623 self.add_target_args(&mut command);
624 command_display(&command)
625 }
626
627 #[must_use]
629 pub fn stop_canister_display(&self, canister: &str) -> String {
630 let mut command = self.canister_command();
631 command.args(["stop", canister]);
632 self.add_target_args(&mut command);
633 command_display(&command)
634 }
635
636 #[must_use]
638 pub fn start_canister_display(&self, canister: &str) -> String {
639 let mut command = self.canister_command();
640 command.args(["start", canister]);
641 self.add_target_args(&mut command);
642 command_display(&command)
643 }
644
645 #[must_use]
647 pub fn canister_top_up_display(&self, canister: &str, amount_cycles: u128) -> String {
648 let mut command = self.canister_command();
649 command.args(["top-up", "--amount"]);
650 command.arg(amount_cycles.to_string());
651 command.arg(canister);
652 self.add_target_args(&mut command);
653 command_display(&command)
654 }
655
656 #[must_use]
658 pub fn canister_query_output_display(
659 &self,
660 canister: &str,
661 method: &str,
662 output: Option<&str>,
663 ) -> String {
664 let mut command = self.canister_command();
665 command.args(["call", canister, method]);
666 command.arg("()");
667 command.arg("--query");
668 if let Some(output) = output {
669 add_output_arg(&mut command, output);
670 }
671 self.add_target_args(&mut command);
672 command_display(&command)
673 }
674
675 #[must_use]
677 pub fn canister_call_arg_output_display(
678 &self,
679 canister: &str,
680 method: &str,
681 arg: &str,
682 output: Option<&str>,
683 ) -> String {
684 let mut command = self.canister_command();
685 command.args(["call", canister, method]);
686 command.arg(arg);
687 if let Some(output) = output {
688 add_output_arg(&mut command, output);
689 }
690 self.add_target_args(&mut command);
691 command_display(&command)
692 }
693
694 fn add_target_args(&self, command: &mut Command) {
695 add_target_args(command, self.environment(), self.network());
696 }
697
698 fn add_local_network_target(&self, command: &mut Command) {
699 if let Some(environment) = self.environment() {
700 command.args(["-e", environment]);
701 } else if let Some(network) = self.network() {
702 command.arg(network);
703 } else {
704 command.arg(LOCAL_NETWORK);
705 }
706 }
707}
708
709#[must_use]
711pub fn default_command() -> Command {
712 IcpCli::new("icp", None, None).command()
713}
714
715#[must_use]
717pub fn default_command_in(cwd: &Path) -> Command {
718 IcpCli::new("icp", None, None).command_in(cwd)
719}
720
721pub fn add_target_args(command: &mut Command, environment: Option<&str>, network: Option<&str>) {
723 if let Some(environment) = environment {
724 if environment == LOCAL_NETWORK
725 && let Some(url) = env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV)
726 {
727 command.env_remove("ICP_ENVIRONMENT");
728 command.arg("-n").arg(url);
729 if let Some(root_key) = env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV) {
730 command.arg("-k").arg(root_key);
731 }
732 return;
733 }
734 command.args(["-e", environment]);
735 } else if let Some(network) = network {
736 command.args(["-n", network]);
737 }
738}
739
740pub fn add_output_arg(command: &mut Command, output: &str) {
742 if output == "json" {
743 command.arg("--json");
744 } else {
745 command.args(["--output", output]);
746 }
747}
748
749pub fn add_debug_arg(command: &mut Command, debug: bool) {
751 if debug {
752 command.arg("--debug");
753 }
754}
755
756fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
757 command.arg("--project-root-override").arg(cwd);
758}
759
760fn run_local_replica_start_command(
761 command: &mut Command,
762 background: bool,
763 debug: bool,
764) -> Result<String, IcpCommandError> {
765 add_debug_arg(command, debug);
766 if background {
767 command.arg("--background");
768 return run_output_with_stderr(command);
769 }
770 run_status_inherit(command)?;
771 Ok(String::new())
772}
773
774pub fn run_output(command: &mut Command) -> Result<String, IcpCommandError> {
776 let display = command_display(command);
777 let output = command.output()?;
778 if output.status.success() {
779 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
780 } else {
781 Err(IcpCommandError::Failed {
782 command: display,
783 stderr: command_stderr(&output),
784 })
785 }
786}
787
788pub fn run_output_with_stderr(command: &mut Command) -> Result<String, IcpCommandError> {
790 let display = command_display(command);
791 let output = command.output()?;
792 if output.status.success() {
793 let mut text = String::from_utf8_lossy(&output.stdout).to_string();
794 text.push_str(&String::from_utf8_lossy(&output.stderr));
795 Ok(text.trim().to_string())
796 } else {
797 Err(IcpCommandError::Failed {
798 command: display,
799 stderr: command_stderr(&output),
800 })
801 }
802}
803
804pub fn run_json<T>(command: &mut Command) -> Result<T, IcpCommandError>
806where
807 T: serde::de::DeserializeOwned,
808{
809 let display = command_display(command);
810 let output = command.output()?;
811 if output.status.success() {
812 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
813 serde_json::from_str(&stdout).map_err(|source| IcpCommandError::Json {
814 command: display,
815 output: stdout,
816 source,
817 })
818 } else {
819 Err(IcpCommandError::Failed {
820 command: display,
821 stderr: command_stderr(&output),
822 })
823 }
824}
825
826pub fn run_status(command: &mut Command) -> Result<(), IcpCommandError> {
828 let display = command_display(command);
829 let output = command.output()?;
830 if output.status.success() {
831 Ok(())
832 } else {
833 Err(IcpCommandError::Failed {
834 command: display,
835 stderr: command_stderr(&output),
836 })
837 }
838}
839
840pub fn run_status_inherit(command: &mut Command) -> Result<(), IcpCommandError> {
842 let display = command_display(command);
843 let mut child = command
844 .stdout(Stdio::inherit())
845 .stderr(Stdio::piped())
846 .spawn()?;
847 let stderr_handle = child
848 .stderr
849 .take()
850 .map(|stderr| thread::spawn(move || stream_and_capture_stderr(stderr)));
851 let status = child.wait()?;
852 let stderr = match stderr_handle {
853 Some(handle) => match handle.join() {
854 Ok(result) => result?,
855 Err(_) => Vec::new(),
856 },
857 None => Vec::new(),
858 };
859 if status.success() {
860 Ok(())
861 } else {
862 let stderr = if stderr.is_empty() {
863 format!("command exited with status {}", exit_status_label(status))
864 } else {
865 String::from_utf8_lossy(&stderr).to_string()
866 };
867 Err(IcpCommandError::Failed {
868 command: display,
869 stderr,
870 })
871 }
872}
873
874fn stream_and_capture_stderr(mut stderr: impl Read) -> io::Result<Vec<u8>> {
875 let mut captured = Vec::new();
876 let mut buffer = [0_u8; 8192];
877 let mut terminal = io::stderr().lock();
878 loop {
879 let read = stderr.read(&mut buffer)?;
880 if read == 0 {
881 break;
882 }
883 terminal.write_all(&buffer[..read])?;
884 captured.extend_from_slice(&buffer[..read]);
885 }
886 terminal.flush()?;
887 Ok(captured)
888}
889
890pub fn run_success(command: &mut Command) -> Result<bool, IcpCommandError> {
892 Ok(command.output()?.status.success())
893}
894
895pub fn run_raw_output(program: &str, args: &[String]) -> Result<IcpRawOutput, std::io::Error> {
897 let output = Command::new(program).args(args).output()?;
898 Ok(IcpRawOutput {
899 success: output.status.success(),
900 status: exit_status_label(output.status),
901 stdout: output.stdout,
902 stderr: output.stderr,
903 })
904}
905
906#[must_use]
908pub fn command_display(command: &Command) -> String {
909 let mut parts = vec![command.get_program().to_string_lossy().to_string()];
910 parts.extend(
911 command
912 .get_args()
913 .map(|arg| arg.to_string_lossy().to_string()),
914 );
915 parts.join(" ")
916}
917
918#[must_use]
920pub fn parse_snapshot_id(output: &str) -> Option<String> {
921 let trimmed = output.trim();
922 if is_snapshot_id_token(trimmed) {
923 return Some(trimmed.to_string());
924 }
925
926 output
927 .lines()
928 .flat_map(|line| {
929 line.split(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ':' | ','))
930 })
931 .find(|part| is_snapshot_id_token(part))
932 .map(str::to_string)
933}
934
935fn is_snapshot_id_token(value: &str) -> bool {
937 !value.is_empty()
938 && value.len().is_multiple_of(2)
939 && value.chars().all(|c| c.is_ascii_hexdigit())
940}
941
942fn command_stderr(output: &std::process::Output) -> String {
944 let stderr = String::from_utf8_lossy(&output.stderr);
945 if stderr.trim().is_empty() {
946 String::from_utf8_lossy(&output.stdout).to_string()
947 } else {
948 stderr.to_string()
949 }
950}
951
952fn exit_status_label(status: std::process::ExitStatus) -> String {
954 status
955 .code()
956 .map_or_else(|| "signal".to_string(), |code| code.to_string())
957}
958
959#[cfg(test)]
960mod tests;