1use crate::icp::{IcpCli, IcpCommandError, IcpJsonResponseError};
8use candid::CandidType;
9use serde::de::DeserializeOwned;
10#[cfg(unix)]
11use std::os::unix::fs::OpenOptionsExt;
12use std::{
13 fs::{self, OpenOptions},
14 io::{self, Write},
15 path::{Path, PathBuf},
16 sync::atomic::{AtomicU64, Ordering},
17};
18use thiserror::Error as ThisError;
19
20const MAX_ARGUMENT_FILE_ATTEMPTS: usize = 32;
21static NEXT_ARGUMENT_FILE: AtomicU64 = AtomicU64::new(0);
22
23#[derive(Debug, ThisError)]
26pub enum IcpCandidCallError {
27 #[error("failed to encode Candid call argument: {0}")]
28 Encode(#[source] candid::Error),
29
30 #[error("failed to manage Candid call argument file: {0}")]
31 File(#[source] io::Error),
32
33 #[error(transparent)]
34 Icp(#[from] IcpCommandError),
35
36 #[error("failed to decode Candid call response: {0}")]
37 Response(#[source] IcpJsonResponseError),
38}
39
40impl IcpCli {
41 pub fn canister_call_candid<I, O>(
43 &self,
44 canister: &str,
45 method: &str,
46 input: &I,
47 candid_path: Option<&Path>,
48 ) -> Result<O, IcpCandidCallError>
49 where
50 I: CandidType,
51 O: CandidType + DeserializeOwned,
52 {
53 self.canister_candid(canister, method, input, candid_path, false)
54 }
55
56 pub fn canister_query_candid<I, O>(
58 &self,
59 canister: &str,
60 method: &str,
61 input: &I,
62 candid_path: Option<&Path>,
63 ) -> Result<O, IcpCandidCallError>
64 where
65 I: CandidType,
66 O: CandidType + DeserializeOwned,
67 {
68 self.canister_candid(canister, method, input, candid_path, true)
69 }
70
71 fn canister_candid<I, O>(
72 &self,
73 canister: &str,
74 method: &str,
75 input: &I,
76 candid_path: Option<&Path>,
77 query: bool,
78 ) -> Result<O, IcpCandidCallError>
79 where
80 I: CandidType,
81 O: CandidType + DeserializeOwned,
82 {
83 let bytes = candid::encode_one(input).map_err(IcpCandidCallError::Encode)?;
84 let path = write_candid_argument_file(&bytes).map_err(IcpCandidCallError::File)?;
85 let output = if query {
86 self.canister_query_binary_args_output_with_candid(
87 canister,
88 method,
89 &path,
90 Some("json"),
91 candid_path,
92 )
93 } else {
94 self.canister_call_binary_args_output_with_candid(
95 canister,
96 method,
97 &path,
98 Some("json"),
99 candid_path,
100 )
101 };
102 let cleanup = fs::remove_file(&path);
103 let output = output?;
104 cleanup.map_err(IcpCandidCallError::File)?;
105 crate::icp::response::decode_json_response(&output).map_err(IcpCandidCallError::Response)
106 }
107}
108
109pub fn write_candid_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
114 for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
115 let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
116 let path = std::env::temp_dir().join(format!(
117 "canic-raw-candid-{}-{sequence}.bin",
118 std::process::id()
119 ));
120 let mut options = OpenOptions::new();
121 options.write(true).create_new(true);
122 #[cfg(unix)]
123 options.mode(0o600);
124 match options.open(&path) {
125 Ok(mut file) => {
126 if let Err(source) = file.write_all(bytes) {
127 drop(file);
128 let _ = fs::remove_file(&path);
129 return Err(source);
130 }
131 return Ok(path);
132 }
133 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
134 Err(source) => return Err(source),
135 }
136 }
137 Err(io::Error::new(
138 io::ErrorKind::AlreadyExists,
139 "could not allocate a unique Candid argument file",
140 ))
141}
142
143#[cfg(all(test, unix))]
148mod tests {
149 use super::*;
150 use canic_core::cdk::utils::hash::hex_bytes;
151 use std::{collections::BTreeSet, os::unix::fs::PermissionsExt};
152
153 #[test]
154 fn child_reads_complete_arguments_and_transport_always_removes_scratch() {
155 let root = crate::test_support::temp_dir("candid-child-arguments");
156 fs::create_dir_all(&root).unwrap();
157 let executable = root.join("icp");
158 fs::write(
159 &executable,
160 r#"#!/bin/sh
161if [ "$1" = --version ]; then echo 'icp 1.5.0'; exit 0; fi
162query=false
163while [ "$#" -gt 0 ]; do
164 case "$1" in
165 --args-file) shift; argument=$1 ;;
166 --query) query=true ;;
167 esac
168 shift
169done
170printf '%s\n' "$argument" > argument-path
171printf '%s\n' "$query" > query-mode
172cp "$argument" received || exit 91
173cat response
174exit "$(cat exit-code)"
175"#,
176 )
177 .unwrap();
178 fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
179 let icp = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
180 let payload = vec![0xa5_u8; 1024 * 1024];
181 let encoded = candid::encode_one(&payload).unwrap();
182 let response = serde_json::json!({
183 "response_bytes": hex_bytes(candid::encode_one(42_u64).unwrap()),
184 });
185 let mut paths = BTreeSet::new();
186 for query in [false, true] {
187 for outcome in 0..3 {
188 fs::write(root.join("exit-code"), if outcome == 1 { "1" } else { "0" }).unwrap();
189 fs::write(
190 root.join("response"),
191 if outcome == 2 {
192 "{}".to_string()
193 } else {
194 response.to_string()
195 },
196 )
197 .unwrap();
198 let result =
199 icp.canister_candid::<_, u64>("aaaaa-aa", "probe", &payload, None, query);
200 match outcome {
201 0 => assert_eq!(result.unwrap(), 42),
202 1 => assert!(matches!(result, Err(IcpCandidCallError::Icp(_)))),
203 _ => assert!(matches!(result, Err(IcpCandidCallError::Response(_)))),
204 }
205 assert_eq!(fs::read(root.join("received")).unwrap(), encoded);
206 assert_eq!(
207 fs::read_to_string(root.join("query-mode")).unwrap().trim(),
208 query.to_string()
209 );
210 let path = PathBuf::from(
211 fs::read_to_string(root.join("argument-path"))
212 .unwrap()
213 .trim(),
214 );
215 assert!(!path.exists());
216 assert!(paths.insert(path));
217 }
218 }
219 fs::remove_dir_all(root).unwrap();
220 }
221}