freenet_stdlib/host_imports.rs
1//! The authoritative list of WASM host imports this crate declares.
2//!
3//! # Why this exists
4//!
5//! A Rust `extern "C"` declaration becomes a WASM **import** only if something
6//! calls it, and it is resolved **by name at module instantiation**. So a crate
7//! can declare an import no host provides, and nothing notices: the SDK
8//! compiles, the host compiles, both publish, and CI stays green on both sides.
9//! It surfaces only when someone writes a contract or delegate that calls the
10//! function and watches it fail to instantiate — by which point the SDK has
11//! been telling authors to use it, in released documentation.
12//!
13//! That is not hypothetical. freenet-stdlib 0.10.0 shipped seven delegate host
14//! imports that freenet-core 0.2.136 does not register: three withdrawn by
15//! freenet-core#5638, and four that no released node ever provided. One of the
16//! four, `__frnt__delegate__subscribe_contract_checked`, was documented as the
17//! *preferred* alternative to a function that did work. They accumulated
18//! because nothing compared the two sides. See freenet-stdlib#133.
19//!
20//! # What this gives you
21//!
22//! [`DECLARED_HOST_IMPORTS`] is a hand-maintained list, and
23//! `host_import_manifest_tests` parses this crate's own source to check that
24//! the list and the `extern "C"` blocks agree. Adding or removing an import
25//! therefore cannot be silent: it fails the build until someone edits this
26//! list, which puts the change in the diff a reviewer reads.
27//!
28//! # The other half lives in freenet-core
29//!
30//! This guard proves only that the list matches *what the SDK declares*. It
31//! cannot see what the host registers. freenet-core should assert its own
32//! linker registration set against this constant — it is `pub`, and compiled
33//! into the crate freenet-core already depends on, precisely so that check
34//! needs no cross-repo file plumbing. Tracked in freenet-core#5717.
35//!
36//! As of this release the two sets agree exactly: the 13 `__frnt__delegate__*`
37//! entries below are the 13 registered by `WasmtimeEngine::register_host_functions`
38//! at freenet-core 0.2.136.
39
40/// One WASM host import: the import module it is resolved in, and its name.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42pub struct HostImport {
43 /// The `wasm_import_module` the host must register this function under.
44 pub module: &'static str,
45 /// The import's link name.
46 pub name: &'static str,
47}
48
49impl HostImport {
50 const fn new(module: &'static str, name: &'static str) -> Self {
51 Self { module, name }
52 }
53}
54
55/// Every WASM host import declared by this crate, sorted by `(module, name)`.
56///
57/// A host that runs contracts must register the `freenet_*` entries below that
58/// a contract can reach; a host that runs delegates must register the
59/// `freenet_delegate_*` entries. An import declared here and absent from the
60/// host is a load-time failure for any guest that calls it.
61///
62/// **Editing this list is the point.** It is checked against the `extern "C"`
63/// blocks by `host_import_manifest_tests`, so it is not documentation that can
64/// drift — but it is also not generated, so a change here is a deliberate act
65/// that appears in review.
66pub const DECLARED_HOST_IMPORTS: &[HostImport] = &[
67 HostImport::new("freenet_contract_io", "__frnt__fill_buffer"),
68 HostImport::new(
69 "freenet_delegate_contracts",
70 "__frnt__delegate__get_contract_state",
71 ),
72 HostImport::new(
73 "freenet_delegate_contracts",
74 "__frnt__delegate__get_contract_state_len",
75 ),
76 HostImport::new("freenet_delegate_ctx", "__frnt__delegate__ctx_len"),
77 HostImport::new("freenet_delegate_ctx", "__frnt__delegate__ctx_read"),
78 HostImport::new("freenet_delegate_ctx", "__frnt__delegate__ctx_write"),
79 HostImport::new(
80 "freenet_delegate_management",
81 "__frnt__delegate__create_delegate",
82 ),
83 HostImport::new("freenet_delegate_secrets", "__frnt__delegate__get_secret"),
84 HostImport::new(
85 "freenet_delegate_secrets",
86 "__frnt__delegate__get_secret_len",
87 ),
88 HostImport::new("freenet_delegate_secrets", "__frnt__delegate__has_secret"),
89 HostImport::new("freenet_delegate_secrets", "__frnt__delegate__list_secrets"),
90 HostImport::new(
91 "freenet_delegate_secrets",
92 "__frnt__delegate__list_secrets_len",
93 ),
94 HostImport::new(
95 "freenet_delegate_secrets",
96 "__frnt__delegate__remove_secret",
97 ),
98 HostImport::new("freenet_delegate_secrets", "__frnt__delegate__set_secret"),
99 HostImport::new("freenet_log", "__frnt__logger__info"),
100 HostImport::new("freenet_rand", "__frnt__rand__rand_bytes"),
101 HostImport::new("freenet_time", "__frnt__time__utc_now"),
102];
103
104#[cfg(test)]
105mod host_import_manifest_tests {
106 use super::{HostImport, DECLARED_HOST_IMPORTS};
107
108 /// Every source file in this crate that declares host imports.
109 ///
110 /// Held as `include_str!` rather than read from disk so the test works from
111 /// a packaged crate, and so adding a file here is a compile-time act.
112 ///
113 /// A new file with an `extern "C"` block and no entry here would be missed.
114 /// [`every_extern_c_block_is_in_a_scanned_file`] is the backstop for that:
115 /// it walks the whole `src/` tree on disk and fails if any file outside
116 /// this list contains a host-import block.
117 const SCANNED: &[(&str, &str)] = &[
118 ("delegate_host.rs", include_str!("delegate_host.rs")),
119 ("host_imports.rs", include_str!("host_imports.rs")),
120 ("log.rs", include_str!("log.rs")),
121 ("rand.rs", include_str!("rand.rs")),
122 ("time.rs", include_str!("time.rs")),
123 ("memory/buf.rs", include_str!("memory/buf.rs")),
124 ];
125
126 /// Drop everything from the first `#[cfg(test)]` onward.
127 ///
128 /// A host import is never declared inside a test module, but a *fixture*
129 /// for this parser is: the tests below contain `extern "C"` blocks as
130 /// string literals, and this very file would otherwise be read as
131 /// declaring `__frnt__delegate__real` and friends. Stripping test code
132 /// first is what lets the guard scan its own source honestly rather than
133 /// carve out an exception for it.
134 fn strip_test_modules(src: &str) -> &str {
135 match src.find("#[cfg(test)]") {
136 Some(i) => &src[..i],
137 None => src,
138 }
139 }
140
141 /// A line inside an `extern "C"` block that the parser did not understand.
142 ///
143 /// Recorded as an entry rather than ignored, so it can never match
144 /// [`DECLARED_HOST_IMPORTS`] and therefore turns the guard **red**. A
145 /// parser that silently skips what it cannot read is the failure mode this
146 /// whole module exists to prevent, one level down: it would be a check that
147 /// passes because it saw nothing.
148 const UNPARSED: &str = "<unparsed-extern-line>";
149
150 /// Strip a leading visibility qualifier, returning the rest of the line.
151 ///
152 /// Handles `pub`, `pub(crate)`, `pub(super)`, `pub(in some::path)` and any
153 /// other parenthesised restriction. An earlier version matched only the
154 /// literal prefixes `fn `, `pub fn ` and `pub(crate) fn `, so a
155 /// `pub(super) fn` import was skipped entirely — and because the
156 /// whole-tree scan uses this same parser, such an import stayed invisible
157 /// even in a new file. Found by an external reviewer on PR #134.
158 fn strip_visibility(t: &str) -> &str {
159 let Some(rest) = t.strip_prefix("pub") else {
160 return t;
161 };
162 // `pub` must be a whole word: `pubfn` is not a visibility.
163 let rest = match rest.chars().next() {
164 Some('(') => match rest.find(')') {
165 Some(i) => &rest[i + 1..],
166 // An unterminated `pub(` is not something we can read; hand
167 // back the original so it is reported as unparsed rather than
168 // quietly treated as a bare `fn`.
169 None => return t,
170 },
171 Some(c) if c.is_whitespace() => rest,
172 _ => return t,
173 };
174 rest.trim_start()
175 }
176
177 /// True if `t`, ignoring a trailing `//` line comment, ends with `;`.
178 ///
179 /// Used to detect where a declaration's signature closes. A trailing
180 /// comment after the closing `;` (`fn __frnt__x() -> i32; // deprecated`)
181 /// is ordinary Rust, but matching the raw line's `ends_with(';')` treats
182 /// it as still open: `in_signature` never clears, so every following
183 /// line up to the block's `}` — including a genuine `fn` declaration —
184 /// is silently swallowed as "part of this signature" instead of being
185 /// read or reported as [`UNPARSED`]. That is exactly the failure mode
186 /// this module exists to not have. Found in review of PR #134.
187 fn ends_with_semicolon_ignoring_trailing_comment(t: &str) -> bool {
188 let core = match t.find("//") {
189 Some(i) => t[..i].trim_end(),
190 None => t,
191 };
192 core.ends_with(';')
193 }
194
195 /// Parse `#[link(wasm_import_module = "M")] ... extern "C" { fn NAME(..) }`
196 /// out of Rust source.
197 ///
198 /// Deliberately matches only a `fn` **declaration line inside an extern
199 /// block**, never a bare occurrence of the name. This file and
200 /// `delegate_host.rs` mention these identifiers dozens of times in prose,
201 /// and a check satisfied by its own doc comments is not a check.
202 ///
203 /// Inside an extern block the parser is **fail-closed**: blank lines, doc
204 /// comments, attributes and the continuation lines of a multi-line
205 /// signature are skipped, a `fn` declaration is recorded, and anything else
206 /// is recorded as [`UNPARSED`] so the guard fails loudly instead of missing
207 /// a declaration it did not recognise.
208 fn parse_imports(src: &str) -> Vec<(String, String)> {
209 let mut out = Vec::new();
210 let mut pending_module: Option<String> = None;
211 let mut current_module: Option<String> = None;
212 let mut in_extern = false;
213 // True while we are inside a signature spread over several lines, i.e.
214 // after a `fn ...(` whose line did not terminate with `;`.
215 let mut in_signature = false;
216
217 for line in src.lines() {
218 let t = line.trim();
219
220 if in_extern {
221 if t == "}" || t.starts_with("} ") {
222 in_extern = false;
223 in_signature = false;
224 current_module = None;
225 continue;
226 }
227 if in_signature {
228 if ends_with_semicolon_ignoring_trailing_comment(t) {
229 in_signature = false;
230 }
231 continue;
232 }
233 if t.is_empty() || t.starts_with("//") || t.starts_with("#[") {
234 continue;
235 }
236
237 let decl = strip_visibility(t);
238 if let Some(rest) = decl.strip_prefix("fn ").or_else(|| {
239 // `fn` with no trailing space, e.g. `fn__` is not valid, but
240 // `unsafe fn` inside extern is.
241 decl.strip_prefix("unsafe fn ")
242 }) {
243 let name: String = rest
244 .chars()
245 .take_while(|c| c.is_alphanumeric() || *c == '_')
246 .collect();
247 if name.is_empty() {
248 out.push((UNPARSED.to_string(), t.to_string()));
249 } else {
250 // An extern block with no `#[link]` resolves in "env".
251 // Recording it as such makes an unattributed block show
252 // up as a mismatch rather than vanish.
253 let module = current_module.clone().unwrap_or_else(|| "env".to_string());
254 out.push((module, name));
255 if !ends_with_semicolon_ignoring_trailing_comment(t) {
256 in_signature = true;
257 }
258 }
259 } else {
260 // Could be a `static`, a `type`, or a declaration shape this
261 // parser has never seen. Either way it is not something to
262 // pass over in silence.
263 out.push((UNPARSED.to_string(), t.to_string()));
264 }
265 continue;
266 }
267
268 if t.starts_with("#[link(") && t.contains("wasm_import_module") {
269 // Take the first quoted string after the `=`.
270 if let Some(eq) = t.find('=') {
271 let after = &t[eq + 1..];
272 if let Some(open) = after.find('"') {
273 let rest = &after[open + 1..];
274 if let Some(close) = rest.find('"') {
275 pending_module = Some(rest[..close].to_string());
276 }
277 }
278 }
279 continue;
280 }
281
282 // `unsafe extern "C"` is the Rust 2024 spelling of the same thing.
283 //
284 // Only a BLOCK opens here. `extern "C" fn name(..) {` is a
285 // definition of a function this crate exports, not a declaration of
286 // one it imports, and reading its body as if it were a block is how
287 // a stray `0` from `buf.rs`'s off-wasm stub first appeared as a
288 // phantom import. So require that nothing but the brace follows.
289 let opener = t
290 .strip_prefix("unsafe extern \"C\"")
291 .or_else(|| t.strip_prefix("extern \"C\""));
292 if let Some(rest) = opener {
293 let rest = rest.trim();
294 if rest.is_empty() || rest == "{" {
295 in_extern = true;
296 in_signature = false;
297 current_module = pending_module.take();
298 } else {
299 // A definition, e.g. `extern "C" fn foo() {`. Not an import
300 // block, and it must not consume the pending `#[link]`.
301 pending_module = None;
302 }
303 continue;
304 }
305
306 // Anything else that is not an attribute clears a dangling
307 // `#[link]`, so the module cannot leak onto an unrelated block.
308 if !t.starts_with("#[") && !t.is_empty() {
309 pending_module = None;
310 }
311 }
312
313 out
314 }
315
316 fn declared_from_source() -> Vec<(String, String)> {
317 let mut found: Vec<(String, String)> = SCANNED
318 .iter()
319 .flat_map(|(_, src)| parse_imports(strip_test_modules(src)))
320 .collect();
321 found.sort();
322 found.dedup();
323 found
324 }
325
326 /// The guard. The `extern "C"` blocks and [`DECLARED_HOST_IMPORTS`] must
327 /// name exactly the same set.
328 ///
329 /// If this fails, do not "fix" it by editing the list to match. Ask first
330 /// whether freenet-core registers the import — an import the host does not
331 /// provide is a load-time failure for any guest that calls it, which is the
332 /// failure this whole module exists to prevent.
333 #[test]
334 fn the_declared_manifest_matches_the_extern_blocks() {
335 let from_source = declared_from_source();
336
337 let mut from_manifest: Vec<(String, String)> = DECLARED_HOST_IMPORTS
338 .iter()
339 .map(|i| (i.module.to_string(), i.name.to_string()))
340 .collect();
341 from_manifest.sort();
342
343 let missing: Vec<_> = from_source
344 .iter()
345 .filter(|i| !from_manifest.contains(i))
346 .collect();
347 let extra: Vec<_> = from_manifest
348 .iter()
349 .filter(|i| !from_source.contains(i))
350 .collect();
351
352 assert!(
353 missing.is_empty() && extra.is_empty(),
354 "host import manifest is out of step with the extern \"C\" blocks.\n\
355 Declared in source but absent from DECLARED_HOST_IMPORTS: {missing:?}\n\
356 Listed in DECLARED_HOST_IMPORTS but not declared in source: {extra:?}\n\
357 \n\
358 Adding an entry is only correct if freenet-core registers it. See \
359 the module docs."
360 );
361 }
362
363 /// The manifest must be sorted and free of duplicates, so a diff against it
364 /// is readable and two entries cannot disagree.
365 #[test]
366 fn the_manifest_is_sorted_and_unique() {
367 let mut sorted = DECLARED_HOST_IMPORTS.to_vec();
368 sorted.sort();
369 assert_eq!(
370 DECLARED_HOST_IMPORTS,
371 sorted.as_slice(),
372 "DECLARED_HOST_IMPORTS must be sorted by (module, name)"
373 );
374
375 let mut seen = sorted.clone();
376 seen.dedup();
377 assert_eq!(
378 seen.len(),
379 DECLARED_HOST_IMPORTS.len(),
380 "DECLARED_HOST_IMPORTS contains duplicates"
381 );
382 }
383
384 /// The seven imports removed in 0.11.0 must stay gone.
385 ///
386 /// A named regression test rather than a comment, because the way each of
387 /// these arrived was a plausible-looking addition to the extern block. The
388 /// guard above would catch a re-add as a manifest mismatch; this says, in
389 /// the failure message, why it is not simply a list that needs updating.
390 #[test]
391 fn the_imports_removed_in_0_11_0_have_not_come_back() {
392 const REMOVED: &[&str] = &[
393 "__frnt__delegate__put_contract_state",
394 "__frnt__delegate__update_contract_state",
395 "__frnt__delegate__subscribe_contract",
396 "__frnt__delegate__subscribe_contract_checked",
397 "__frnt__delegate__list_subscriptions_len",
398 "__frnt__delegate__list_subscriptions",
399 "__frnt__delegate__schedule_wakeup",
400 ];
401
402 let from_source = declared_from_source();
403 for name in REMOVED {
404 assert!(
405 !from_source.iter().any(|(_, n)| n == name),
406 "`{name}` was removed in 0.11.0 because no released freenet-core \
407 registers it; a delegate calling it fails to instantiate. \
408 Re-adding it needs the host side to exist first."
409 );
410 assert!(
411 !DECLARED_HOST_IMPORTS.iter().any(|i| i.name == *name),
412 "`{name}` is back in DECLARED_HOST_IMPORTS; see freenet-stdlib#133"
413 );
414 }
415 }
416
417 /// The parser must not be satisfied by prose.
418 ///
419 /// `delegate_host.rs` names its imports repeatedly in doc comments, so a
420 /// scraper that matched bare occurrences would pass while the extern block
421 /// said something else entirely. This pins that it does not.
422 #[test]
423 fn prose_mentioning_an_import_is_not_read_as_a_declaration() {
424 let src = r#"
425/// Calls `__frnt__delegate__ghost` under the hood, see fn __frnt__delegate__phantom
426// fn __frnt__delegate__commented_out(a: i32) -> i32;
427#[cfg(target_family = "wasm")]
428#[link(wasm_import_module = "freenet_real")]
429extern "C" {
430 /// Doc mentioning fn __frnt__delegate__not_this
431 fn __frnt__delegate__real(a: i32) -> i32;
432}
433
434fn __frnt__delegate__local_definition() -> i64 { 0 }
435"#;
436 assert_eq!(
437 parse_imports(src),
438 vec![(
439 "freenet_real".to_string(),
440 "__frnt__delegate__real".to_string()
441 )],
442 "only a `fn` declaration line inside an extern block is an import"
443 );
444 }
445
446 /// A restricted visibility must not hide an import.
447 ///
448 /// The parser originally matched only `fn `, `pub fn ` and `pub(crate) fn `,
449 /// so `pub(super) fn` was skipped — and since the whole-tree scan shares
450 /// this parser, an import declared that way was invisible to every check
451 /// here. A guard with a blind spot is worse than no guard, because it is
452 /// trusted. Found by an external reviewer on PR #134.
453 #[test]
454 fn every_visibility_spelling_is_recognised() {
455 for vis in [
456 "",
457 "pub ",
458 "pub(crate) ",
459 "pub(super) ",
460 "pub(self) ",
461 "pub(in crate::memory) ",
462 ] {
463 let src = format!(
464 "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {{\n {vis}fn __frnt__x() -> i32;\n}}\n"
465 );
466 assert_eq!(
467 parse_imports(&src),
468 vec![("freenet_m".to_string(), "__frnt__x".to_string())],
469 "visibility {vis:?} was not recognised"
470 );
471 }
472 }
473
474 /// Anything inside an extern block the parser cannot read is reported, not
475 /// skipped.
476 ///
477 /// This is what makes the guard fail-closed. A parser that silently passes
478 /// over a declaration shape it has never seen is a check that succeeds
479 /// because it looked at nothing — the same defect one level down from the
480 /// one this module exists to catch.
481 #[test]
482 fn an_unreadable_declaration_is_reported_rather_than_skipped() {
483 let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n static SOMETHING: i32;\n}\n";
484 let parsed = parse_imports(src);
485 assert_eq!(parsed.len(), 1);
486 assert_eq!(parsed[0].0, UNPARSED, "unreadable line must be flagged");
487
488 // And it must make the real guard red, not merely be recorded.
489 assert!(
490 !DECLARED_HOST_IMPORTS.iter().any(|i| i.module == UNPARSED),
491 "UNPARSED must never be a legitimate manifest module"
492 );
493 }
494
495 /// A signature spread over several lines yields one import, and its
496 /// argument lines are not mistaken for declarations.
497 #[test]
498 fn a_multi_line_signature_is_one_import_and_its_arguments_are_not() {
499 let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n fn __frnt__wide(\n a: i64,\n b: i32,\n ) -> i64;\n fn __frnt__narrow() -> i32;\n}\n";
500 assert_eq!(
501 parse_imports(src),
502 vec![
503 ("freenet_m".to_string(), "__frnt__wide".to_string()),
504 ("freenet_m".to_string(), "__frnt__narrow".to_string()),
505 ]
506 );
507 }
508
509 /// An `extern "C" fn` DEFINITION is not an import block.
510 ///
511 /// `memory/buf.rs` defines an off-wasm stub as
512 /// `unsafe extern "C" fn __frnt__fill_buffer(..) { .. }`. Reading that as a
513 /// block opener walks into the function body, where a bare `0` was briefly
514 /// reported as a phantom import. Exporting a function and importing one are
515 /// opposite things and must not share a code path.
516 #[test]
517 fn an_extern_c_function_definition_is_not_an_import_block() {
518 let src = "#[no_mangle]\nunsafe extern \"C\" fn __frnt__stub(_a: i64) -> u32 {\n 0\n}\n";
519 assert_eq!(parse_imports(src), vec![]);
520
521 let src = "#[no_mangle]\nextern \"C\" fn __frnt__stub2() -> u32 {\n 0\n}\n";
522 assert_eq!(parse_imports(src), vec![]);
523
524 // And the real file must contain exactly its one declared import.
525 let buf = strip_test_modules(include_str!("memory/buf.rs"));
526 assert_eq!(
527 parse_imports(buf),
528 vec![(
529 "freenet_contract_io".to_string(),
530 "__frnt__fill_buffer".to_string()
531 )],
532 "buf.rs declares one import and defines one stub of the same name"
533 );
534 }
535
536 /// A trailing line comment after the semicolon that closes a
537 /// declaration must not be mistaken for the signature still being open.
538 ///
539 /// Two independent review lenses on PR #134 found the same latent gap:
540 /// `ends_with(';')` on the raw line failed for
541 /// `fn __frnt__first() -> i32; // trailing comment`, leaving
542 /// `in_signature` stuck and silently swallowing every following line —
543 /// including a genuine `fn` — up to the block's `}`. Covers both the
544 /// single-line declaration case and the multi-line-signature closing
545 /// line, since the bug lived in both call sites of the same check.
546 #[test]
547 fn a_trailing_comment_after_the_closing_semicolon_does_not_swallow_the_next_declaration() {
548 let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n fn __frnt__first() -> i32; // trailing comment\n fn __frnt__second() -> i32;\n}\n";
549 assert_eq!(
550 parse_imports(src),
551 vec![
552 ("freenet_m".to_string(), "__frnt__first".to_string()),
553 ("freenet_m".to_string(), "__frnt__second".to_string()),
554 ],
555 "a trailing comment on the closing line must not hide the next import"
556 );
557
558 let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n fn __frnt__wide(\n a: i64,\n ) -> i64; // trailing comment\n fn __frnt__narrow() -> i32;\n}\n";
559 assert_eq!(
560 parse_imports(src),
561 vec![
562 ("freenet_m".to_string(), "__frnt__wide".to_string()),
563 ("freenet_m".to_string(), "__frnt__narrow".to_string()),
564 ],
565 "a trailing comment on a multi-line signature's closing line must not hide the next import"
566 );
567 }
568
569 /// `unsafe extern "C"` is the Rust 2024 spelling and must parse the same.
570 #[test]
571 fn the_rust_2024_unsafe_extern_spelling_is_recognised() {
572 let src = "#[link(wasm_import_module = \"freenet_m\")]\nunsafe extern \"C\" {\n fn __frnt__x() -> i32;\n}\n";
573 assert_eq!(
574 parse_imports(src),
575 vec![("freenet_m".to_string(), "__frnt__x".to_string())]
576 );
577 }
578
579 /// A `#[link]` attribute must not leak onto a later, unrelated extern block.
580 #[test]
581 fn a_link_attribute_does_not_leak_past_intervening_code() {
582 let src = r#"
583#[link(wasm_import_module = "freenet_first")]
584extern "C" {
585 fn __frnt__one() -> i32;
586}
587
588pub fn something_in_between() {}
589
590extern "C" {
591 fn __frnt__two() -> i32;
592}
593"#;
594 assert_eq!(
595 parse_imports(src),
596 vec![
597 ("freenet_first".to_string(), "__frnt__one".to_string()),
598 // No `#[link]`, so it resolves in "env" — and shows as a
599 // mismatch rather than silently inheriting the module above.
600 ("env".to_string(), "__frnt__two".to_string()),
601 ]
602 );
603 }
604
605 /// No source file outside [`SCANNED`] declares host imports.
606 ///
607 /// Without this, adding a new module with an `extern "C"` block would be
608 /// invisible to the guard — the exact silence the guard exists to remove.
609 /// Walks `src/` on disk, so it is skipped when the tree is not present
610 /// (a packaged-crate build), where the `include_str!` set is fixed anyway.
611 #[test]
612 fn every_extern_c_block_is_in_a_scanned_file() {
613 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
614 if !root.is_dir() {
615 return;
616 }
617
618 let known: Vec<String> = SCANNED
619 .iter()
620 .map(|(p, _)| p.replace('/', std::path::MAIN_SEPARATOR_STR))
621 .collect();
622
623 let mut unscanned = Vec::new();
624 let mut stack = vec![root.clone()];
625 while let Some(dir) = stack.pop() {
626 for entry in std::fs::read_dir(&dir).expect("read src/") {
627 let path = entry.expect("dir entry").path();
628 if path.is_dir() {
629 stack.push(path);
630 continue;
631 }
632 if path.extension().and_then(|e| e.to_str()) != Some("rs") {
633 continue;
634 }
635 let rel = path
636 .strip_prefix(&root)
637 .expect("under src/")
638 .to_string_lossy()
639 .to_string();
640 if known.contains(&rel) {
641 continue;
642 }
643 let src = std::fs::read_to_string(&path).expect("read source");
644 if !parse_imports(strip_test_modules(&src)).is_empty() {
645 unscanned.push(rel);
646 }
647 }
648 }
649
650 assert!(
651 unscanned.is_empty(),
652 "these files declare host imports but are not in SCANNED, so the \
653 manifest guard cannot see them: {unscanned:?}"
654 );
655 }
656
657 /// `HostImport` is reachable on the published API, since freenet-core is
658 /// meant to assert its registration set against it.
659 #[test]
660 fn the_manifest_is_public_api() {
661 let one: HostImport = DECLARED_HOST_IMPORTS[0];
662 assert!(one.module.starts_with("freenet_"));
663 assert!(one.name.starts_with("__frnt__"));
664 }
665}