ic_wasm/check_endpoints/
mod.rs1mod candid;
2
3pub use crate::check_endpoints::candid::CandidParser;
4use crate::{info::ExportedMethodInfo, utils::get_exported_methods};
5use anyhow::anyhow;
6use parse_display::{Display, FromStr};
7use std::io::{BufRead, BufReader};
8use std::str::FromStr;
9use std::{collections::BTreeSet, path::Path};
10use walrus::Module;
11
12#[derive(Clone, Eq, Debug, Ord, PartialEq, PartialOrd, Display, FromStr)]
13pub enum CanisterEndpoint {
14 #[display("canister_update:{0}")]
15 Update(String),
16 #[display("canister_query:{0}")]
17 Query(String),
18 #[display("canister_composite_query:{0}")]
19 CompositeQuery(String),
20 #[display("{0}")]
21 Entrypoint(String),
22}
23
24impl TryFrom<&ExportedMethodInfo> for CanisterEndpoint {
25 type Error = anyhow::Error;
26
27 fn try_from(method: &ExportedMethodInfo) -> Result<Self, Self::Error> {
28 type EndpointConstructor = fn(&str) -> CanisterEndpoint;
29 const MAPPINGS: &[(&str, EndpointConstructor)] = &[
30 ("canister_update", |s| {
31 CanisterEndpoint::Update(s.to_string())
32 }),
33 ("canister_query", |s| CanisterEndpoint::Query(s.to_string())),
34 ("canister_composite_query", |s| {
35 CanisterEndpoint::CompositeQuery(s.to_string())
36 }),
37 ];
38
39 for (candid_prefix, constructor) in MAPPINGS {
40 if let Some(rest) = method.name.strip_prefix(candid_prefix) {
41 return Ok(constructor(rest.trim()));
42 }
43 }
44
45 let trimmed = method.name.trim();
46 if !trimmed.is_empty() {
47 Ok(CanisterEndpoint::Entrypoint(trimmed.to_string()))
48 } else {
49 Err(anyhow!("Exported method in canister WASM has empty name"))
50 }
51 }
52}
53
54pub fn check_endpoints(
55 module: &Module,
56 candid_path: Option<&Path>,
57 hidden_path: Option<&Path>,
58) -> anyhow::Result<()> {
59 let wasm_endpoints = get_exported_methods(module)
60 .iter()
61 .map(CanisterEndpoint::try_from)
62 .collect::<Result<BTreeSet<CanisterEndpoint>, _>>()?;
63
64 let candid_endpoints = CandidParser::try_from_wasm(module)?
65 .or_else(|| candid_path.map(CandidParser::from_candid_file))
66 .ok_or(anyhow!(
67 "Candid interface not specified in WASM file and Candid file not provided"
68 ))?
69 .parse()?;
70
71 let missing_candid_endpoints = candid_endpoints
72 .difference(&wasm_endpoints)
73 .collect::<BTreeSet<_>>();
74 missing_candid_endpoints.iter().for_each(|endpoint| {
75 eprintln!(
76 "ERROR: The following Candid endpoint is missing from the WASM exports section: {endpoint}"
77 );
78 });
79
80 let hidden_endpoints = read_hidden_endpoints(hidden_path)?;
81 let missing_hidden_endpoints = hidden_endpoints
82 .difference(&wasm_endpoints)
83 .collect::<BTreeSet<_>>();
84 missing_hidden_endpoints.iter().for_each(|endpoint| {
85 eprintln!(
86 "ERROR: The following hidden endpoint is missing from the WASM exports section: {endpoint}"
87 );
88 });
89
90 let unexpected_endpoints = wasm_endpoints
91 .iter()
92 .filter(|endpoint| {
93 !candid_endpoints.contains(endpoint) && !hidden_endpoints.contains(endpoint)
94 })
95 .collect::<BTreeSet<_>>();
96 unexpected_endpoints.iter().for_each(|endpoint| {
97 eprintln!(
98 "ERROR: The following endpoint is unexpected in the WASM exports section: {endpoint}"
99 );
100 });
101
102 if !missing_candid_endpoints.is_empty()
103 || !missing_hidden_endpoints.is_empty()
104 || !unexpected_endpoints.is_empty()
105 {
106 Err(anyhow!("Canister WASM and Candid interface do not match!"))
107 } else {
108 println!("Canister WASM and Candid interface match!");
109 Ok(())
110 }
111}
112
113fn read_hidden_endpoints(maybe_path: Option<&Path>) -> anyhow::Result<BTreeSet<CanisterEndpoint>> {
114 if let Some(path) = maybe_path {
115 let file = std::fs::File::open(path)
116 .map_err(|e| anyhow!("Failed to read hidden endpoints file: {e:?}"))?;
117 let reader = BufReader::new(file);
118 let lines = reader
119 .lines()
120 .collect::<Result<Vec<_>, _>>()
121 .map_err(|e| anyhow!("Failed to read hidden endpoints file: {e:?}"))?;
122 let endpoints = lines
123 .iter()
124 .map(|line| line.trim())
125 .filter(|line| !line.is_empty())
126 .map(CanisterEndpoint::from_str)
127 .collect::<Result<BTreeSet<_>, _>>()
128 .map_err(|e| anyhow!("Failed to parse hidden endpoints from file: {e:?}"))?;
129 Ok(endpoints)
130 } else {
131 Ok(BTreeSet::new())
132 }
133}