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_arg(
96 icp,
97 binding,
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_arg(
117 icp,
118 binding,
119 canister,
120 method,
121 input,
122 ProtocolCallMode::Query,
123 )
124}
125
126fn invoke_with_arg<I, O>(
127 icp: &IcpCli,
128 binding: &ResolvedProtocolBinding,
129 canister: Principal,
130 method: &'static str,
131 input: &I,
132 mode: ProtocolCallMode,
133) -> Result<O, CanisterProtocolError>
134where
135 I: CandidType,
136 O: CandidType + DeserializeOwned,
137{
138 let bytes =
139 candid::encode_one(input).map_err(|source| CanisterProtocolError::ArgumentEncoding {
140 canister,
141 method,
142 source,
143 })?;
144 let args_path =
145 write_argument_file(&bytes).map_err(|source| CanisterProtocolError::ArgumentFile {
146 canister,
147 method,
148 source,
149 })?;
150 let canister_text = canister.to_text();
151 let output = match mode {
152 ProtocolCallMode::Query => icp.canister_query_binary_args_output_with_candid(
153 &canister_text,
154 method,
155 &args_path,
156 Some(ICP_JSON_OUTPUT),
157 Some(&binding.candid_path),
158 ),
159 ProtocolCallMode::Update => icp.canister_call_binary_args_output_with_candid(
160 &canister_text,
161 method,
162 &args_path,
163 Some(ICP_JSON_OUTPUT),
164 Some(&binding.candid_path),
165 ),
166 };
167 let cleanup = fs::remove_file(&args_path);
168 let output = output.map_err(|source| CanisterProtocolError::Invocation {
169 canister,
170 method,
171 source,
172 })?;
173 cleanup.map_err(|source| CanisterProtocolError::ArgumentFile {
174 canister,
175 method,
176 source,
177 })?;
178 decode_response(canister, method, &output)
179}
180
181fn write_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
182 let directory = std::env::temp_dir();
183 for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
184 let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
185 let path = directory.join(format!(
186 "canic-candid-args-{}-{sequence}.bin",
187 std::process::id()
188 ));
189 match create_argument_file(&path, bytes) {
190 Ok(()) => return Ok(path),
191 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
192 Err(source) => return Err(source),
193 }
194 }
195 Err(io::Error::new(
196 io::ErrorKind::AlreadyExists,
197 "could not allocate a unique Candid argument file",
198 ))
199}
200
201fn create_argument_file(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
202 let mut options = OpenOptions::new();
203 options.write(true).create_new(true);
204 #[cfg(unix)]
205 options.mode(0o600);
206 let mut file = options.open(path)?;
207 if let Err(source) = file.write_all(bytes).and_then(|()| file.sync_all()) {
208 drop(file);
209 let _ = fs::remove_file(path);
210 return Err(source);
211 }
212 Ok(())
213}
214
215fn decode_response<O>(
216 canister: Principal,
217 method: &'static str,
218 output: &str,
219) -> Result<O, CanisterProtocolError>
220where
221 O: CandidType + DeserializeOwned,
222{
223 decode_json_result_response(output).map_err(|source| CanisterProtocolError::Response {
224 canister,
225 method,
226 source,
227 })
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use serde::Deserialize;
234
235 #[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
236 struct EmptyVectorArgument {
237 values: Vec<u64>,
238 }
239
240 #[test]
241 fn typed_arguments_are_written_to_private_binary_files() {
242 let argument = EmptyVectorArgument { values: Vec::new() };
243 let path = write_argument_file(&candid::encode_one(&argument).expect("encode argument"))
244 .expect("write argument file");
245
246 let decoded: EmptyVectorArgument =
247 candid::decode_one(&fs::read(&path).expect("read argument file"))
248 .expect("decode argument");
249 assert_eq!(decoded, argument);
250 #[cfg(unix)]
251 {
252 use std::os::unix::fs::PermissionsExt;
253
254 assert_eq!(
255 fs::metadata(&path)
256 .expect("argument metadata")
257 .permissions()
258 .mode()
259 & 0o777,
260 0o600
261 );
262 }
263 fs::remove_file(path).expect("remove argument file");
264 }
265}