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 cleanup_candid_argument_file(&path);
103 let output = output?;
104 crate::icp::response::decode_json_response(&output).map_err(IcpCandidCallError::Response)
105 }
106}
107
108pub fn cleanup_candid_argument_file(path: &Path) {
110 if let Err(error) = fs::remove_file(path)
111 && error.kind() != io::ErrorKind::NotFound
112 {
113 eprintln!(
114 "warning: could not remove Candid argument file {}: {error}",
115 path.display()
116 );
117 }
118}
119
120pub fn write_candid_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
125 for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
126 let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
127 let path = std::env::temp_dir().join(format!(
128 "canic-raw-candid-{}-{sequence}.bin",
129 std::process::id()
130 ));
131 let mut options = OpenOptions::new();
132 options.write(true).create_new(true);
133 #[cfg(unix)]
134 options.mode(0o600);
135 match options.open(&path) {
136 Ok(mut file) => {
137 if let Err(source) = file.write_all(bytes) {
138 drop(file);
139 let _ = fs::remove_file(&path);
140 return Err(source);
141 }
142 return Ok(path);
143 }
144 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
145 Err(source) => return Err(source),
146 }
147 }
148 Err(io::Error::new(
149 io::ErrorKind::AlreadyExists,
150 "could not allocate a unique Candid argument file",
151 ))
152}
153
154#[cfg(all(test, unix))]
159mod tests {
160 use super::*;
161 use canic_core::cdk::utils::hash::hex_bytes;
162 use std::{collections::BTreeSet, os::unix::fs::PermissionsExt};
163
164 #[test]
165 fn child_reads_complete_arguments_and_cleanup_preserves_transport_outcomes() {
166 let root = crate::test_support::temp_dir("candid-child-arguments");
167 fs::create_dir_all(&root).unwrap();
168 let executable = root.join("icp");
169 fs::write(
170 &executable,
171 crate::test_support::tool_script(
172 r#"#!/bin/sh
173if [ "$1" = --version ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
174query=false
175while [ "$#" -gt 0 ]; do
176 case "$1" in
177 --args-file) shift; argument=$1 ;;
178 --query) query=true ;;
179 esac
180 shift
181done
182printf '%s\n' "$argument" > argument-path
183printf '%s\n' "$query" > query-mode
184cp "$argument" received || exit 91
185case "$(cat cleanup-mode)" in
186 missing) rm "$argument" ;;
187 directory) rm "$argument"; mkdir "$argument" ;;
188esac
189cat response
190exit "$(cat exit-code)"
191"#,
192 ),
193 )
194 .unwrap();
195 fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
196 let icp = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
197 let payload = vec![0xa5_u8; 1024 * 1024];
198 let encoded = candid::encode_one(&payload).unwrap();
199 let response = serde_json::json!({
200 "response_bytes": hex_bytes(candid::encode_one(42_u64).unwrap()),
201 });
202 let mut paths = BTreeSet::new();
203 for (query, cleanup_mode) in [
204 (false, "normal"),
205 (true, "normal"),
206 (false, "missing"),
207 (true, "missing"),
208 (false, "directory"),
209 (true, "directory"),
210 ] {
211 fs::write(root.join("cleanup-mode"), cleanup_mode).unwrap();
212 for outcome in 0..3 {
213 fs::write(root.join("exit-code"), if outcome == 1 { "1" } else { "0" }).unwrap();
214 fs::write(
215 root.join("response"),
216 if outcome == 2 {
217 "{}".to_string()
218 } else {
219 response.to_string()
220 },
221 )
222 .unwrap();
223 let result =
224 icp.canister_candid::<_, u64>("aaaaa-aa", "probe", &payload, None, query);
225 match outcome {
226 0 => assert_eq!(result.unwrap(), 42),
227 1 => assert!(matches!(result, Err(IcpCandidCallError::Icp(_)))),
228 _ => assert!(matches!(result, Err(IcpCandidCallError::Response(_)))),
229 }
230 assert_eq!(fs::read(root.join("received")).unwrap(), encoded);
231 assert_eq!(
232 fs::read_to_string(root.join("query-mode")).unwrap().trim(),
233 query.to_string()
234 );
235 let path = PathBuf::from(
236 fs::read_to_string(root.join("argument-path"))
237 .unwrap()
238 .trim(),
239 );
240 if cleanup_mode == "directory" {
241 assert!(path.is_dir());
242 fs::remove_dir(&path).unwrap();
243 } else {
244 assert!(!path.exists());
245 }
246 assert!(paths.insert(path));
247 }
248 }
249 fs::remove_dir_all(root).unwrap();
250 }
251}