1use crate::{
9 icp::{
10 IcpCli, IcpCommandError, IcpJsonResponseError, decode_json_result_response, response_bytes,
11 },
12 protocol_binding::ResolvedProtocolBinding,
13};
14use candid::{CandidType, Principal};
15use canic_core::diagnostics::RegisteredDiagnosticCode;
16use serde::de::DeserializeOwned;
17#[cfg(unix)]
18use std::os::unix::fs::OpenOptionsExt;
19use std::{
20 fs::{self, OpenOptions},
21 io::{self, Write},
22 path::PathBuf,
23 sync::atomic::{AtomicU64, Ordering},
24};
25use thiserror::Error as ThisError;
26
27const ICP_JSON_OUTPUT: &str = "json";
28const MAX_ARGUMENT_FILE_ATTEMPTS: usize = 32;
29static NEXT_ARGUMENT_FILE: AtomicU64 = AtomicU64::new(0);
30
31#[derive(Clone, Copy)]
32enum ProtocolCallMode {
33 Query,
34 Update,
35}
36
37#[derive(Debug, ThisError)]
38pub enum CanisterProtocolError {
39 #[error("failed to encode Candid arguments for {method} on Canister {canister}: {source}")]
40 ArgumentEncoding {
41 canister: Principal,
42 method: &'static str,
43 #[source]
44 source: candid::Error,
45 },
46
47 #[error(
48 "failed to manage binary Candid arguments for {method} on Canister {canister}: {source}"
49 )]
50 ArgumentFile {
51 canister: Principal,
52 method: &'static str,
53 #[source]
54 source: io::Error,
55 },
56
57 #[error("failed to invoke {method} on Canister {canister}: {source}")]
58 Invocation {
59 canister: Principal,
60 method: &'static str,
61 #[source]
62 source: IcpCommandError,
63 },
64
65 #[error("invalid {method} response from Canister {canister}: {source}")]
66 Response {
67 canister: Principal,
68 method: &'static str,
69 #[source]
70 source: IcpJsonResponseError,
71 },
72}
73
74impl CanisterProtocolError {
75 pub(crate) fn is_rejected_with(&self, code: RegisteredDiagnosticCode) -> bool {
76 matches!(
77 self,
78 Self::Response {
79 source: IcpJsonResponseError::Rejected(error),
80 ..
81 } if error.code() == code.raw_code()
82 )
83 }
84}
85
86pub fn call_with_arg<I, O>(
87 icp: &IcpCli,
88 binding: &ResolvedProtocolBinding,
89 canister: Principal,
90 method: &'static str,
91 input: &I,
92) -> Result<O, CanisterProtocolError>
93where
94 I: CandidType,
95 O: CandidType + DeserializeOwned,
96{
97 invoke_with_candid(
98 icp,
99 &binding.candid_path,
100 canister,
101 method,
102 input,
103 ProtocolCallMode::Update,
104 )
105}
106
107pub fn query_with_arg<I, O>(
108 icp: &IcpCli,
109 binding: &ResolvedProtocolBinding,
110 canister: Principal,
111 method: &'static str,
112 input: &I,
113) -> Result<O, CanisterProtocolError>
114where
115 I: CandidType,
116 O: CandidType + DeserializeOwned,
117{
118 invoke_with_candid(
119 icp,
120 &binding.candid_path,
121 canister,
122 method,
123 input,
124 ProtocolCallMode::Query,
125 )
126}
127
128pub fn call_with_candid<I, O>(
131 icp: &IcpCli,
132 candid_path: &std::path::Path,
133 canister: Principal,
134 method: &'static str,
135 input: &I,
136) -> Result<O, CanisterProtocolError>
137where
138 I: CandidType,
139 O: CandidType + DeserializeOwned,
140{
141 invoke_with_candid(
142 icp,
143 candid_path,
144 canister,
145 method,
146 input,
147 ProtocolCallMode::Update,
148 )
149}
150
151pub fn query_with_candid<I, O>(
154 icp: &IcpCli,
155 candid_path: &std::path::Path,
156 canister: Principal,
157 method: &'static str,
158 input: &I,
159) -> Result<O, CanisterProtocolError>
160where
161 I: CandidType,
162 O: CandidType + DeserializeOwned,
163{
164 invoke_with_candid(
165 icp,
166 candid_path,
167 canister,
168 method,
169 input,
170 ProtocolCallMode::Query,
171 )
172}
173
174pub fn query_response_bytes<I>(
179 icp: &IcpCli,
180 canister: Principal,
181 method: &'static str,
182 input: &I,
183) -> Result<Vec<u8>, CanisterProtocolError>
184where
185 I: CandidType,
186{
187 let output = invoke_output(icp, None, canister, method, input, ProtocolCallMode::Query)?;
188 response_bytes(&output).map_err(|source| CanisterProtocolError::Response {
189 canister,
190 method,
191 source,
192 })
193}
194
195fn invoke_with_candid<I, O>(
196 icp: &IcpCli,
197 candid_path: &std::path::Path,
198 canister: Principal,
199 method: &'static str,
200 input: &I,
201 mode: ProtocolCallMode,
202) -> Result<O, CanisterProtocolError>
203where
204 I: CandidType,
205 O: CandidType + DeserializeOwned,
206{
207 let output = invoke_output(icp, Some(candid_path), canister, method, input, mode)?;
208 decode_response(canister, method, &output)
209}
210
211fn invoke_output<I>(
212 icp: &IcpCli,
213 candid_path: Option<&std::path::Path>,
214 canister: Principal,
215 method: &'static str,
216 input: &I,
217 mode: ProtocolCallMode,
218) -> Result<String, CanisterProtocolError>
219where
220 I: CandidType,
221{
222 let bytes =
223 candid::encode_one(input).map_err(|source| CanisterProtocolError::ArgumentEncoding {
224 canister,
225 method,
226 source,
227 })?;
228 let args_path =
229 write_argument_file(&bytes).map_err(|source| CanisterProtocolError::ArgumentFile {
230 canister,
231 method,
232 source,
233 })?;
234 let canister_text = canister.to_text();
235 let output = match mode {
236 ProtocolCallMode::Query => icp.canister_query_binary_args_output_with_candid(
237 &canister_text,
238 method,
239 &args_path,
240 Some(ICP_JSON_OUTPUT),
241 candid_path,
242 ),
243 ProtocolCallMode::Update => icp.canister_call_binary_args_output_with_candid(
244 &canister_text,
245 method,
246 &args_path,
247 Some(ICP_JSON_OUTPUT),
248 candid_path,
249 ),
250 };
251 let cleanup = fs::remove_file(&args_path);
252 let output = output.map_err(|source| CanisterProtocolError::Invocation {
253 canister,
254 method,
255 source,
256 })?;
257 cleanup.map_err(|source| CanisterProtocolError::ArgumentFile {
258 canister,
259 method,
260 source,
261 })?;
262 Ok(output)
263}
264
265fn write_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
266 let directory = std::env::temp_dir();
267 for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
268 let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
269 let path = directory.join(format!(
270 "canic-candid-args-{}-{sequence}.bin",
271 std::process::id()
272 ));
273 match create_argument_file(&path, bytes) {
274 Ok(()) => return Ok(path),
275 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
276 Err(source) => return Err(source),
277 }
278 }
279 Err(io::Error::new(
280 io::ErrorKind::AlreadyExists,
281 "could not allocate a unique Candid argument file",
282 ))
283}
284
285fn create_argument_file(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
286 let mut options = OpenOptions::new();
287 options.write(true).create_new(true);
288 #[cfg(unix)]
289 options.mode(0o600);
290 let mut file = options.open(path)?;
291 if let Err(source) = file.write_all(bytes).and_then(|()| file.sync_all()) {
292 drop(file);
293 let _ = fs::remove_file(path);
294 return Err(source);
295 }
296 Ok(())
297}
298
299fn decode_response<O>(
300 canister: Principal,
301 method: &'static str,
302 output: &str,
303) -> Result<O, CanisterProtocolError>
304where
305 O: CandidType + DeserializeOwned,
306{
307 decode_json_result_response(output).map_err(|source| CanisterProtocolError::Response {
308 canister,
309 method,
310 source,
311 })
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use serde::Deserialize;
318
319 #[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
320 struct EmptyVectorArgument {
321 values: Vec<u64>,
322 }
323
324 #[test]
325 fn typed_arguments_are_written_to_private_binary_files() {
326 let argument = EmptyVectorArgument { values: Vec::new() };
327 let path = write_argument_file(&candid::encode_one(&argument).expect("encode argument"))
328 .expect("write argument file");
329
330 let decoded: EmptyVectorArgument =
331 candid::decode_one(&fs::read(&path).expect("read argument file"))
332 .expect("decode argument");
333 assert_eq!(decoded, argument);
334 #[cfg(unix)]
335 {
336 use std::os::unix::fs::PermissionsExt;
337
338 assert_eq!(
339 fs::metadata(&path)
340 .expect("argument metadata")
341 .permissions()
342 .mode()
343 & 0o777,
344 0o600
345 );
346 }
347 fs::remove_file(path).expect("remove argument file");
348 }
349}