ic_wasm/check_endpoints/
mod.rs

1mod 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}