Skip to content

Commit 64db3a5

Browse files
authored
chore: add support for comments in hidden endpoint file (#98)
* DEFI-2488: Add support for comments in hidden endpoints * DEFI-2488: Update README
1 parent 10f1b59 commit 64db3a5

File tree

4 files changed

+82
-35
lines changed

4 files changed

+82
-35
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ path = "src/bin/main.rs"
2222
required-features = ["exe"]
2323

2424
[features]
25-
check-endpoints = ["dep:anyhow", "dep:candid_parser", "dep:parse-display"]
25+
check-endpoints = ["dep:anyhow", "dep:candid_parser", "dep:parse-display", "dep:serde_json"]
2626
default = ["check-endpoints", "exe", "wasm-opt"]
2727
exe = ["dep:anyhow", "dep:clap", "dep:serde"]
2828
wasm-opt = ["dep:wasm-opt", "dep:tempfile"]

README.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,32 @@ Verify the endpoints a canister’s WASM exports against its Candid interface. T
9797
Usage: `ic-wasm <input.wasm> check-endpoints [--candid <file>] [--hidden <file>]`
9898

9999
- `--candid <file>` (optional) specifies a Candid file containing the canister's expected interface. If omitted, the Candid interface is assumed to be embedded in the WASM file.
100-
- `--hidden <file>` (optional) specifies a file listing endpoints that are intentionally exported by the canister but not present in the Candid interface. Each endpoint should be on a separate line, using one of the following formats:
101-
- `canister_update:<endpoint name>`
102-
- `canister_query:<endpoint name>`
103-
- `canister_composite_query:<endpoint name>`
104-
- `<endpoint name>`
105-
106-
**Example `hidden.txt`:**
107-
```text
108-
canister_update:__motoko_async_helper
109-
canister_query:__get_candid_interface_tmp_hack
110-
canister_query:__motoko_stable_var_info
111-
canister_global_timer
112-
canister_init
113-
canister_post_upgrade
114-
canister_pre_upgrade
115-
```
100+
- `--hidden <file>` (optional) specifies a file that lists endpoints which are intentionally exported by the canister but not included in the Candid interface. Each line describes a single endpoint using one of the following formats:
101+
- `canister_update:<endpoint name>`
102+
- `canister_query:<endpoint name>`
103+
- `canister_composite_query:<endpoint name>`
104+
- `<endpoint name>`
105+
106+
Lines beginning with `#` are treated as comments and ignored.
107+
108+
To include special characters (for example `#` or newlines), the entire line may be wrapped in double quotes (`"`).
109+
When quoted this way, the line is parsed using standard JSON string syntax (see [RFC 8259 section 7](https://www.rfc-editor.org/rfc/rfc8259#section-7)).
110+
111+
**Example `hidden.txt`:**
112+
```text
113+
# A canister update endpoint named `__motoko_async_helper`
114+
canister_update:__motoko_async_helper
115+
116+
# Canister query endpoints named `__get_candid_interface_tmp_hack` and `__motoko_stable_var_info`
117+
canister_query:__get_candid_interface_tmp_hack
118+
canister_query:__motoko_stable_var_info
119+
120+
# Other canister endpoints: a timer, init method, etc.
121+
canister_global_timer
122+
canister_init
123+
canister_post_upgrade
124+
canister_pre_upgrade
125+
```
116126

117127
### Instrument (experimental)
118128

src/check_endpoints/mod.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ pub use crate::check_endpoints::candid::CandidParser;
44
use crate::{info::ExportedMethodInfo, utils::get_exported_methods};
55
use anyhow::anyhow;
66
use parse_display::{Display, FromStr};
7-
use std::io::{BufRead, BufReader};
8-
use std::str::FromStr;
9-
use std::{collections::BTreeSet, path::Path};
7+
use std::io::BufReader;
8+
use std::{collections::BTreeSet, io::BufRead, path::Path, str::FromStr};
109
use walrus::Module;
1110

1211
#[derive(Clone, Eq, Debug, Ord, PartialEq, PartialOrd, Display, FromStr)]
@@ -112,22 +111,57 @@ pub fn check_endpoints(
112111

113112
fn read_hidden_endpoints(maybe_path: Option<&Path>) -> anyhow::Result<BTreeSet<CanisterEndpoint>> {
114113
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:?}"))?;
114+
let mut endpoints = BTreeSet::new();
115+
for line in read_lines(path)? {
116+
if let Some(endpoint) = parse_line(line)? {
117+
endpoints.insert(endpoint);
118+
}
119+
}
129120
Ok(endpoints)
130121
} else {
131122
Ok(BTreeSet::new())
132123
}
133124
}
125+
126+
fn read_lines(path: &Path) -> anyhow::Result<Vec<String>> {
127+
let file = std::fs::File::open(path)
128+
.map_err(|e| anyhow!("Could not open hidden endpoints file: {e:?}"))?;
129+
130+
let reader = BufReader::new(file);
131+
let mut lines = Vec::new();
132+
133+
for line in reader.lines() {
134+
let line = line?;
135+
let trimmed = line.trim();
136+
if !trimmed.is_empty() {
137+
lines.push(trimmed.to_string());
138+
}
139+
}
140+
141+
Ok(lines)
142+
}
143+
144+
fn parse_line(line: String) -> anyhow::Result<Option<CanisterEndpoint>> {
145+
fn parse_uncommented_line(line: String) -> anyhow::Result<Option<CanisterEndpoint>> {
146+
CanisterEndpoint::from_str(line.as_str())
147+
.map(Some)
148+
.map_err(Into::into)
149+
}
150+
// Comment: ignore line
151+
if line.starts_with("#") {
152+
return Ok(None);
153+
}
154+
// Quoted line: use JSON string syntax to allow for character escaping
155+
if line.starts_with('"') {
156+
if !line.ends_with('"') || line.len() < 2 {
157+
return Err(anyhow!(
158+
"Could not parse hidden endpoint, missing terminating quote: {line}"
159+
));
160+
}
161+
return serde_json::from_str::<String>(&line)
162+
.map_err(|e| anyhow!("Could not parse hidden endpoint: {e:?}"))
163+
.and_then(parse_uncommented_line);
164+
}
165+
// Regular line
166+
parse_uncommented_line(line)
167+
}

tests/tests.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,9 @@ fn check_endpoints() {
422422
)
423423
.failure();
424424
const HIDDEN_1: &str = r#"
425+
# Canister update method (this line is a comment)
425426
canister_update:set
427+
# Canister query method (this line is also a comment)
426428
canister_query:get
427429
"#;
428430
wasm_input("wat.wasm.gz", false)
@@ -472,7 +474,8 @@ fn check_endpoints() {
472474
canister_global_timer
473475
canister_init
474476
canister_post_upgrade
475-
canister_pre_upgrade
477+
# The line below is quoted, it is parsed as a JSON string (this line is a comment)
478+
"canister_pre_upgrade"
476479
"#;
477480
wasm_input("motoko.wasm", false)
478481
.arg("check-endpoints")

0 commit comments

Comments
 (0)