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    ///
391    /// `schedule_wakeup` stays on this list even though wake-ups came back in
392    /// 0.12.1: they came back as a MANIFEST entry (`DelegateManifest::wakeups`),
393    /// precisely so a delegate using them does not import anything a node
394    /// might lack. Re-adding the import would make every such delegate fail to
395    /// instantiate on nodes that predate it, which is the failure this list
396    /// exists to prevent.
397    #[test]
398    fn the_imports_removed_in_0_11_0_have_not_come_back() {
399        const REMOVED: &[&str] = &[
400            "__frnt__delegate__put_contract_state",
401            "__frnt__delegate__update_contract_state",
402            "__frnt__delegate__subscribe_contract",
403            "__frnt__delegate__subscribe_contract_checked",
404            "__frnt__delegate__list_subscriptions_len",
405            "__frnt__delegate__list_subscriptions",
406            "__frnt__delegate__schedule_wakeup",
407        ];
408
409        let from_source = declared_from_source();
410        for name in REMOVED {
411            assert!(
412                !from_source.iter().any(|(_, n)| n == name),
413                "`{name}` was removed in 0.11.0 because no released freenet-core \
414                 registers it; a delegate calling it fails to instantiate. \
415                 Re-adding it needs the host side to exist first (wake-ups are \
416                 requested through the manifest since 0.12.1, not this import)."
417            );
418            assert!(
419                !DECLARED_HOST_IMPORTS.iter().any(|i| i.name == *name),
420                "`{name}` is back in DECLARED_HOST_IMPORTS; see freenet-stdlib#133"
421            );
422        }
423    }
424
425    /// The parser must not be satisfied by prose.
426    ///
427    /// `delegate_host.rs` names its imports repeatedly in doc comments, so a
428    /// scraper that matched bare occurrences would pass while the extern block
429    /// said something else entirely. This pins that it does not.
430    #[test]
431    fn prose_mentioning_an_import_is_not_read_as_a_declaration() {
432        let src = r#"
433/// Calls `__frnt__delegate__ghost` under the hood, see fn __frnt__delegate__phantom
434// fn __frnt__delegate__commented_out(a: i32) -> i32;
435#[cfg(target_family = "wasm")]
436#[link(wasm_import_module = "freenet_real")]
437extern "C" {
438    /// Doc mentioning fn __frnt__delegate__not_this
439    fn __frnt__delegate__real(a: i32) -> i32;
440}
441
442fn __frnt__delegate__local_definition() -> i64 { 0 }
443"#;
444        assert_eq!(
445            parse_imports(src),
446            vec![(
447                "freenet_real".to_string(),
448                "__frnt__delegate__real".to_string()
449            )],
450            "only a `fn` declaration line inside an extern block is an import"
451        );
452    }
453
454    /// A restricted visibility must not hide an import.
455    ///
456    /// The parser originally matched only `fn `, `pub fn ` and `pub(crate) fn `,
457    /// so `pub(super) fn` was skipped — and since the whole-tree scan shares
458    /// this parser, an import declared that way was invisible to every check
459    /// here. A guard with a blind spot is worse than no guard, because it is
460    /// trusted. Found by an external reviewer on PR #134.
461    #[test]
462    fn every_visibility_spelling_is_recognised() {
463        for vis in [
464            "",
465            "pub ",
466            "pub(crate) ",
467            "pub(super) ",
468            "pub(self) ",
469            "pub(in crate::memory) ",
470        ] {
471            let src = format!(
472                "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {{\n    {vis}fn __frnt__x() -> i32;\n}}\n"
473            );
474            assert_eq!(
475                parse_imports(&src),
476                vec![("freenet_m".to_string(), "__frnt__x".to_string())],
477                "visibility {vis:?} was not recognised"
478            );
479        }
480    }
481
482    /// Anything inside an extern block the parser cannot read is reported, not
483    /// skipped.
484    ///
485    /// This is what makes the guard fail-closed. A parser that silently passes
486    /// over a declaration shape it has never seen is a check that succeeds
487    /// because it looked at nothing — the same defect one level down from the
488    /// one this module exists to catch.
489    #[test]
490    fn an_unreadable_declaration_is_reported_rather_than_skipped() {
491        let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n    static SOMETHING: i32;\n}\n";
492        let parsed = parse_imports(src);
493        assert_eq!(parsed.len(), 1);
494        assert_eq!(parsed[0].0, UNPARSED, "unreadable line must be flagged");
495
496        // And it must make the real guard red, not merely be recorded.
497        assert!(
498            !DECLARED_HOST_IMPORTS.iter().any(|i| i.module == UNPARSED),
499            "UNPARSED must never be a legitimate manifest module"
500        );
501    }
502
503    /// A signature spread over several lines yields one import, and its
504    /// argument lines are not mistaken for declarations.
505    #[test]
506    fn a_multi_line_signature_is_one_import_and_its_arguments_are_not() {
507        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";
508        assert_eq!(
509            parse_imports(src),
510            vec![
511                ("freenet_m".to_string(), "__frnt__wide".to_string()),
512                ("freenet_m".to_string(), "__frnt__narrow".to_string()),
513            ]
514        );
515    }
516
517    /// An `extern "C" fn` DEFINITION is not an import block.
518    ///
519    /// `memory/buf.rs` defines an off-wasm stub as
520    /// `unsafe extern "C" fn __frnt__fill_buffer(..) { .. }`. Reading that as a
521    /// block opener walks into the function body, where a bare `0` was briefly
522    /// reported as a phantom import. Exporting a function and importing one are
523    /// opposite things and must not share a code path.
524    #[test]
525    fn an_extern_c_function_definition_is_not_an_import_block() {
526        let src = "#[no_mangle]\nunsafe extern \"C\" fn __frnt__stub(_a: i64) -> u32 {\n    0\n}\n";
527        assert_eq!(parse_imports(src), vec![]);
528
529        let src = "#[no_mangle]\nextern \"C\" fn __frnt__stub2() -> u32 {\n    0\n}\n";
530        assert_eq!(parse_imports(src), vec![]);
531
532        // And the real file must contain exactly its one declared import.
533        let buf = strip_test_modules(include_str!("memory/buf.rs"));
534        assert_eq!(
535            parse_imports(buf),
536            vec![(
537                "freenet_contract_io".to_string(),
538                "__frnt__fill_buffer".to_string()
539            )],
540            "buf.rs declares one import and defines one stub of the same name"
541        );
542    }
543
544    /// A trailing line comment after the semicolon that closes a
545    /// declaration must not be mistaken for the signature still being open.
546    ///
547    /// Two independent review lenses on PR #134 found the same latent gap:
548    /// `ends_with(';')` on the raw line failed for
549    /// `fn __frnt__first() -> i32; // trailing comment`, leaving
550    /// `in_signature` stuck and silently swallowing every following line —
551    /// including a genuine `fn` — up to the block's `}`. Covers both the
552    /// single-line declaration case and the multi-line-signature closing
553    /// line, since the bug lived in both call sites of the same check.
554    #[test]
555    fn a_trailing_comment_after_the_closing_semicolon_does_not_swallow_the_next_declaration() {
556        let src = "#[link(wasm_import_module = \"freenet_m\")]\nextern \"C\" {\n    fn __frnt__first() -> i32; // trailing comment\n    fn __frnt__second() -> i32;\n}\n";
557        assert_eq!(
558            parse_imports(src),
559            vec![
560                ("freenet_m".to_string(), "__frnt__first".to_string()),
561                ("freenet_m".to_string(), "__frnt__second".to_string()),
562            ],
563            "a trailing comment on the closing line must not hide the next import"
564        );
565
566        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";
567        assert_eq!(
568            parse_imports(src),
569            vec![
570                ("freenet_m".to_string(), "__frnt__wide".to_string()),
571                ("freenet_m".to_string(), "__frnt__narrow".to_string()),
572            ],
573            "a trailing comment on a multi-line signature's closing line must not hide the next import"
574        );
575    }
576
577    /// `unsafe extern "C"` is the Rust 2024 spelling and must parse the same.
578    #[test]
579    fn the_rust_2024_unsafe_extern_spelling_is_recognised() {
580        let src = "#[link(wasm_import_module = \"freenet_m\")]\nunsafe extern \"C\" {\n    fn __frnt__x() -> i32;\n}\n";
581        assert_eq!(
582            parse_imports(src),
583            vec![("freenet_m".to_string(), "__frnt__x".to_string())]
584        );
585    }
586
587    /// A `#[link]` attribute must not leak onto a later, unrelated extern block.
588    #[test]
589    fn a_link_attribute_does_not_leak_past_intervening_code() {
590        let src = r#"
591#[link(wasm_import_module = "freenet_first")]
592extern "C" {
593    fn __frnt__one() -> i32;
594}
595
596pub fn something_in_between() {}
597
598extern "C" {
599    fn __frnt__two() -> i32;
600}
601"#;
602        assert_eq!(
603            parse_imports(src),
604            vec![
605                ("freenet_first".to_string(), "__frnt__one".to_string()),
606                // No `#[link]`, so it resolves in "env" — and shows as a
607                // mismatch rather than silently inheriting the module above.
608                ("env".to_string(), "__frnt__two".to_string()),
609            ]
610        );
611    }
612
613    /// No source file outside [`SCANNED`] declares host imports.
614    ///
615    /// Without this, adding a new module with an `extern "C"` block would be
616    /// invisible to the guard — the exact silence the guard exists to remove.
617    /// Walks `src/` on disk, so it is skipped when the tree is not present
618    /// (a packaged-crate build), where the `include_str!` set is fixed anyway.
619    #[test]
620    fn every_extern_c_block_is_in_a_scanned_file() {
621        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
622        if !root.is_dir() {
623            return;
624        }
625
626        let known: Vec<String> = SCANNED
627            .iter()
628            .map(|(p, _)| p.replace('/', std::path::MAIN_SEPARATOR_STR))
629            .collect();
630
631        let mut unscanned = Vec::new();
632        let mut stack = vec![root.clone()];
633        while let Some(dir) = stack.pop() {
634            for entry in std::fs::read_dir(&dir).expect("read src/") {
635                let path = entry.expect("dir entry").path();
636                if path.is_dir() {
637                    stack.push(path);
638                    continue;
639                }
640                if path.extension().and_then(|e| e.to_str()) != Some("rs") {
641                    continue;
642                }
643                let rel = path
644                    .strip_prefix(&root)
645                    .expect("under src/")
646                    .to_string_lossy()
647                    .to_string();
648                if known.contains(&rel) {
649                    continue;
650                }
651                let src = std::fs::read_to_string(&path).expect("read source");
652                if !parse_imports(strip_test_modules(&src)).is_empty() {
653                    unscanned.push(rel);
654                }
655            }
656        }
657
658        assert!(
659            unscanned.is_empty(),
660            "these files declare host imports but are not in SCANNED, so the \
661             manifest guard cannot see them: {unscanned:?}"
662        );
663    }
664
665    /// `HostImport` is reachable on the published API, since freenet-core is
666    /// meant to assert its registration set against it.
667    #[test]
668    fn the_manifest_is_public_api() {
669        let one: HostImport = DECLARED_HOST_IMPORTS[0];
670        assert!(one.module.starts_with("freenet_"));
671        assert!(one.name.starts_with("__frnt__"));
672    }
673}