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