canic_host/canister_protocol/
mod.rs1use 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 bytes =
185 candid::encode_one(input).map_err(|source| CanisterProtocolError::ArgumentEncoding {
186 canister,
187 method,
188 source,
189 })?;
190 let args_path =
191 write_argument_file(&bytes).map_err(|source| CanisterProtocolError::ArgumentFile {
192 canister,
193 method,
194 source,
195 })?;
196 let canister_text = canister.to_text();
197 let output = match mode {
198 ProtocolCallMode::Query => icp.canister_query_binary_args_output_with_candid(
199 &canister_text,
200 method,
201 &args_path,
202 Some(ICP_JSON_OUTPUT),
203 Some(candid_path),
204 ),
205 ProtocolCallMode::Update => icp.canister_call_binary_args_output_with_candid(
206 &canister_text,
207 method,
208 &args_path,
209 Some(ICP_JSON_OUTPUT),
210 Some(candid_path),
211 ),
212 };
213 let cleanup = fs::remove_file(&args_path);
214 let output = output.map_err(|source| CanisterProtocolError::Invocation {
215 canister,
216 method,
217 source,
218 })?;
219 cleanup.map_err(|source| CanisterProtocolError::ArgumentFile {
220 canister,
221 method,
222 source,
223 })?;
224 decode_response(canister, method, &output)
225}
226
227fn write_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
228 let directory = std::env::temp_dir();
229 for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
230 let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
231 let path = directory.join(format!(
232 "canic-candid-args-{}-{sequence}.bin",
233 std::process::id()
234 ));
235 match create_argument_file(&path, bytes) {
236 Ok(()) => return Ok(path),
237 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
238 Err(source) => return Err(source),
239 }
240 }
241 Err(io::Error::new(
242 io::ErrorKind::AlreadyExists,
243 "could not allocate a unique Candid argument file",
244 ))
245}
246
247fn create_argument_file(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
248 let mut options = OpenOptions::new();
249 options.write(true).create_new(true);
250 #[cfg(unix)]
251 options.mode(0o600);
252 let mut file = options.open(path)?;
253 if let Err(source) = file.write_all(bytes).and_then(|()| file.sync_all()) {
254 drop(file);
255 let _ = fs::remove_file(path);
256 return Err(source);
257 }
258 Ok(())
259}
260
261fn decode_response<O>(
262 canister: Principal,
263 method: &'static str,
264 output: &str,
265) -> Result<O, CanisterProtocolError>
266where
267 O: CandidType + DeserializeOwned,
268{
269 decode_json_result_response(output).map_err(|source| CanisterProtocolError::Response {
270 canister,
271 method,
272 source,
273 })
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use serde::Deserialize;
280
281 #[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
282 struct EmptyVectorArgument {
283 values: Vec<u64>,
284 }
285
286 #[test]
287 fn typed_arguments_are_written_to_private_binary_files() {
288 let argument = EmptyVectorArgument { values: Vec::new() };
289 let path = write_argument_file(&candid::encode_one(&argument).expect("encode argument"))
290 .expect("write argument file");
291
292 let decoded: EmptyVectorArgument =
293 candid::decode_one(&fs::read(&path).expect("read argument file"))
294 .expect("decode argument");
295 assert_eq!(decoded, argument);
296 #[cfg(unix)]
297 {
298 use std::os::unix::fs::PermissionsExt;
299
300 assert_eq!(
301 fs::metadata(&path)
302 .expect("argument metadata")
303 .permissions()
304 .mode()
305 & 0o777,
306 0o600
307 );
308 }
309 fs::remove_file(path).expect("remove argument file");
310 }
311}