pub const HOST_CLOCK_NAMESPACE: &str = "freenet_time";
pub const HOST_CLOCK_IMPORT: &str = "__frnt__time__utc_now";
pub const HOST_CLOCK_DEPRECATION_DOC: &str = "https://github.com/freenet/freenet-core/blob/main/docs/architecture/contracts/README.md#contracts-must-not-read-the-host-clock";
pub fn imports_host_clock(wasm: &[u8]) -> bool {
for payload in wasmparser::Parser::new(0).parse_all(wasm) {
match payload {
Ok(wasmparser::Payload::ImportSection(reader)) => {
for import in reader.into_imports().flatten() {
if import.module == HOST_CLOCK_NAMESPACE && import.name == HOST_CLOCK_IMPORT {
return true;
}
}
return false;
}
Ok(_) => {}
Err(_) => return false,
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn module_importing(imports: &[(&str, &str)]) -> Vec<u8> {
let mut wat = String::from("(module\n");
for (i, (namespace, name)) in imports.iter().enumerate() {
wat.push_str(&format!(
" (import \"{namespace}\" \"{name}\" (func $f{i} (param i64 i64)))\n"
));
}
wat.push_str(")\n");
wat::parse_str(&wat).expect("test fixture is valid wat")
}
#[test]
fn a_module_importing_the_clock_is_detected() {
let wasm = module_importing(&[(HOST_CLOCK_NAMESPACE, HOST_CLOCK_IMPORT)]);
assert!(imports_host_clock(&wasm));
}
#[test]
fn the_clock_is_found_among_other_imports() {
let wasm = module_importing(&[
("freenet_log", "__frnt__logger__info"),
(HOST_CLOCK_NAMESPACE, HOST_CLOCK_IMPORT),
("freenet_rand", "__frnt__rand__rand_bytes"),
]);
assert!(imports_host_clock(&wasm));
}
#[test]
fn a_module_importing_other_host_functions_is_not_flagged() {
let wasm = module_importing(&[
("freenet_log", "__frnt__logger__info"),
("freenet_rand", "__frnt__rand__rand_bytes"),
]);
assert!(!imports_host_clock(&wasm));
}
#[test]
fn namespace_and_function_must_both_match() {
let other_fn = module_importing(&[(HOST_CLOCK_NAMESPACE, "__frnt__time__something_else")]);
assert!(!imports_host_clock(&other_fn));
let other_ns = module_importing(&[("some_other_namespace", HOST_CLOCK_IMPORT)]);
assert!(!imports_host_clock(&other_ns));
}
#[test]
fn a_module_with_no_imports_at_all_is_not_flagged() {
let wasm = module_importing(&[]);
assert!(!imports_host_clock(&wasm));
}
#[test]
fn the_name_merely_appearing_in_the_module_is_not_an_import() {
let wat = format!(
r#"(module
(memory (export "memory") 1)
(data (i32.const 0) "{HOST_CLOCK_NAMESPACE}")
(data (i32.const 64) "{HOST_CLOCK_IMPORT}")
(func (export "{HOST_CLOCK_IMPORT}") (param i64 i64)))"#
);
let wasm = wat::parse_str(&wat).expect("test fixture is valid wat");
assert!(
wasm.windows(HOST_CLOCK_IMPORT.len())
.any(|w| w == HOST_CLOCK_IMPORT.as_bytes()),
"fixture must actually contain the name, or this test proves nothing"
);
assert!(!imports_host_clock(&wasm));
}
#[test]
fn an_unparseable_module_is_not_flagged() {
assert!(!imports_host_clock(b""));
assert!(!imports_host_clock(b"not wasm at all"));
let mut truncated = module_importing(&[(HOST_CLOCK_NAMESPACE, HOST_CLOCK_IMPORT)]);
truncated.truncate(10);
assert!(!imports_host_clock(&truncated));
}
#[test]
fn a_corrupt_tail_does_not_void_an_import_already_read() {
let mut wasm = module_importing(&[(HOST_CLOCK_NAMESPACE, HOST_CLOCK_IMPORT)]);
wasm.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
assert!(
imports_host_clock(&wasm),
"a clock import already read was discarded by a later parse error"
);
}
#[test]
fn a_second_import_section_is_not_scanned_and_the_validator_is_why_that_is_safe() {
let first = module_importing(&[("freenet_log", "__frnt__logger__info")]);
let clock = module_importing(&[(HOST_CLOCK_NAMESPACE, HOST_CLOCK_IMPORT)]);
let section = import_section_of(&clock);
let mut two_sections = first.clone();
two_sections.extend_from_slice(§ion);
assert_ne!(
two_sections, first,
"the fixture must actually add a section"
);
assert!(
!imports_host_clock(&two_sections),
"the short-circuit now scans past the first import section; if that \
is deliberate, remove this test and the caveat it pins"
);
assert!(
wasmparser::validate(&two_sections).is_err(),
"a module with two import sections is now accepted by the validator, \
so the short-circuit's safety argument no longer holds"
);
}
fn import_section_of(wasm: &[u8]) -> Vec<u8> {
for payload in wasmparser::Parser::new(0).parse_all(wasm) {
if let Ok(wasmparser::Payload::ImportSection(reader)) = payload {
let range = reader.range();
let range = (range.start as usize)..(range.end as usize);
let mut out = vec![0x02];
let mut len = (range.end - range.start) as u32;
loop {
let byte = (len & 0x7f) as u8;
len >>= 7;
if len == 0 {
out.push(byte);
break;
}
out.push(byte | 0x80);
}
out.extend_from_slice(&wasm[range]);
return out;
}
}
panic!("fixture has no import section");
}
#[test]
fn the_deprecation_doc_link_points_at_a_heading_that_exists() {
const DOC: &str = include_str!("../../../../docs/architecture/contracts/README.md");
const DOC_PATH: &str = "docs/architecture/contracts/README.md";
assert!(
HOST_CLOCK_DEPRECATION_DOC.contains(DOC_PATH),
"the deprecation link no longer points at {DOC_PATH}, so this test is \
reading a different file from the one operators are sent to: {HOST_CLOCK_DEPRECATION_DOC}"
);
let (_, fragment) = HOST_CLOCK_DEPRECATION_DOC
.split_once('#')
.expect("the deprecation link carries no heading anchor");
fn anchor(heading: &str) -> String {
heading
.trim_start_matches('#')
.trim()
.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-')
.collect::<String>()
.replace(' ', "-")
}
let matches = DOC
.lines()
.filter(|line| line.starts_with('#'))
.filter(|line| anchor(line) == fragment)
.count();
assert_eq!(
matches, 1,
"the anchor `#{fragment}` matches {matches} headings in {DOC_PATH}; the \
node's deprecation warning and every fdev diagnostic link there"
);
}
}