Skip to main content

ic_host_tools/candid/service/
mod.rs

1//! Parser-backed Candid service inventories without expanding argument types.
2//!
3//! The parser owns grammar and type checking. Consumers own source admission,
4//! expected methods, payload annotations and publication. No imports are loaded,
5//! diagnostics printed, files opened or processes run.
6
7use crate::interface::{ExportedMethod, MethodMode};
8use ic_platform::candid_parser::{self, Dec, FuncMode, IDLProg, TypeEnv};
9use std::fmt;
10
11#[cfg(test)]
12mod tests;
13
14/// Caller-selected input and result bounds; zero permits an empty result only.
15///
16/// The source bound applies before parsing. The method bound applies after type
17/// checking and before projecting the inventory. These are not a parser heap,
18/// stack or CPU quota: the selected parser allocates and traverses source types.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct ServiceLimits {
21    /// Maximum UTF-8 source bytes.
22    pub source_bytes: usize,
23    /// Maximum service methods returned.
24    pub methods: usize,
25}
26
27/// One declared service method; arguments and return types are not expanded.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct ServiceMethod {
30    /// Decoded Candid method name, preserving spaces and escaped characters.
31    pub name: String,
32    /// Declared call mode, including oneway.
33    pub mode: MethodMode,
34}
35
36impl ServiceMethod {
37    /// Project this declaration to its Wasm name/mode for caller comparisons.
38    /// This explicitly maps oneway to the update namespace.
39    #[must_use]
40    pub fn as_export(&self) -> ExportedMethod<'_> {
41        ExportedMethod {
42            name: &self.name,
43            mode: self.mode.export_mode(),
44        }
45    }
46}
47
48/// Parser/type-check failure without exposing dependency types in the API.
49#[derive(Debug)]
50pub struct ParseError {
51    source: candid_parser::Error,
52}
53
54impl fmt::Display for ParseError {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        self.source.fmt(formatter)
57    }
58}
59impl std::error::Error for ParseError {}
60
61/// Failure to produce a complete service inventory.
62#[derive(Debug)]
63pub enum ServiceError {
64    /// Source exceeds the caller's byte allowance before parsing.
65    InputLimit {
66        /// Observed bytes.
67        actual: usize,
68        /// Caller-selected maximum.
69        limit: usize,
70    },
71    /// Service exceeds the caller's method allowance before result allocation.
72    MethodLimit {
73        /// Observed methods.
74        actual: usize,
75        /// Caller-selected maximum.
76        limit: usize,
77    },
78    /// Import declarations require an external resolution policy and are refused.
79    ImportsUnsupported,
80    /// The document has no service actor.
81    MissingService,
82    /// Grammar, duplicate definitions or type checking failed.
83    Parse(ParseError),
84    /// The result inventory could not be allocated.
85    Allocation(std::collections::TryReserveError),
86}
87
88impl fmt::Display for ServiceError {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::InputLimit { actual, limit } => write!(
92                formatter,
93                "Candid source has {actual} bytes, exceeding {limit}"
94            ),
95            Self::MethodLimit { actual, limit } => write!(
96                formatter,
97                "Candid service has {actual} methods, exceeding {limit}"
98            ),
99            Self::ImportsUnsupported => {
100                formatter.write_str("Candid imports require caller resolution")
101            }
102            Self::MissingService => formatter.write_str("Candid document has no service"),
103            Self::Parse(source) => source.fmt(formatter),
104            Self::Allocation(_) => formatter.write_str("Candid inventory allocation failed"),
105        }
106    }
107}
108
109impl std::error::Error for ServiceError {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Self::Parse(source) => Some(source),
113            Self::Allocation(source) => Some(source),
114            _ => None,
115        }
116    }
117}
118
119const fn parse_error(source: candid_parser::Error) -> ServiceError {
120    ServiceError::Parse(ParseError { source })
121}
122
123/// Parse and type-check a self-contained document, then return its method facts.
124///
125/// Supports aliases, service constructors, quoted names and parser-supported
126/// comments. Methods are ordered by name by the Candid service type checker.
127/// Recursive/shared argument types remain in the parser's type environment and
128/// are never expanded into a presentation tree. Normalization is not required.
129///
130/// # Errors
131/// Returns input/method bounds, import refusal, missing service, parser/type
132/// errors or result allocation failure. No partial inventory is returned.
133pub fn inspect(source: &str, limits: ServiceLimits) -> Result<Vec<ServiceMethod>, ServiceError> {
134    if source.len() > limits.source_bytes {
135        return Err(ServiceError::InputLimit {
136            actual: source.len(),
137            limit: limits.source_bytes,
138        });
139    }
140    let program = source.parse::<IDLProg>().map_err(parse_error)?;
141    if program
142        .decs
143        .iter()
144        .any(|declaration| matches!(declaration, Dec::ImportType(_) | Dec::ImportServ(_)))
145    {
146        return Err(ServiceError::ImportsUnsupported);
147    }
148    let mut environment = TypeEnv::new();
149    let actor = candid_parser::check_prog(&mut environment, &program)
150        .map_err(parse_error)?
151        .ok_or(ServiceError::MissingService)?;
152    let methods = environment
153        .as_service(&actor)
154        .map_err(|error| parse_error(error.into()))?;
155    if methods.len() > limits.methods {
156        return Err(ServiceError::MethodLimit {
157            actual: methods.len(),
158            limit: limits.methods,
159        });
160    }
161    let mut result = Vec::new();
162    result
163        .try_reserve_exact(methods.len())
164        .map_err(ServiceError::Allocation)?;
165    for (name, ty) in methods {
166        let function = environment
167            .as_func(ty)
168            .map_err(|error| parse_error(error.into()))?;
169        let mode = match function.modes.first() {
170            None => MethodMode::Update,
171            Some(FuncMode::Query) => MethodMode::Query,
172            Some(FuncMode::CompositeQuery) => MethodMode::CompositeQuery,
173            Some(FuncMode::Oneway) => MethodMode::Oneway,
174        };
175        let mut owned_name = String::new();
176        owned_name
177            .try_reserve_exact(name.len())
178            .map_err(ServiceError::Allocation)?;
179        owned_name.push_str(name);
180        result.push(ServiceMethod {
181            name: owned_name,
182            mode,
183        });
184    }
185    Ok(result)
186}