Skip to main content

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}