|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | +// |
| 4 | +//! In-tree codecs for the `typedwasm.regions` and |
| 5 | +//! `typedwasm.access-sites` wasm custom sections (typed-wasm proposals |
| 6 | +//! 0001 + 0002, both `[accepted]` and promoted to ADR-0002 / ADR-0003). |
| 7 | +//! |
| 8 | +//! Like `ownership.rs`, this module deliberately does NOT depend on |
| 9 | +//! `typed-wasm-verify`: the producer and the verifier implement the |
| 10 | +//! shared wire contract independently, and the seam-conformance tests |
| 11 | +//! keep them honest. |
| 12 | +//! |
| 13 | +//! ## The ephapax v1 schema |
| 14 | +//! |
| 15 | +//! Ephapax's only region-allocated runtime values are strings, laid out |
| 16 | +//! as an 8-byte header: `ptr: u32` at +0 (address of the byte buffer) |
| 17 | +//! and `len: u32` at +4. The v1 `typedwasm.regions` table therefore |
| 18 | +//! declares a single region: |
| 19 | +//! |
| 20 | +//! ```text |
| 21 | +//! region 0 "ephapax.string" { field 0 "ptr": U32, field 1 "len": U32 } |
| 22 | +//! ``` |
| 23 | +//! |
| 24 | +//! Both fields are wire-kind Scalar (the byte buffer the `ptr` value |
| 25 | +//! addresses is bump-allocated and not itself a declared region, so the |
| 26 | +//! pointer kinds — which require a valid `target_region` — do not |
| 27 | +//! apply). Every string-header `i32.load` / `i32.store` the codegen |
| 28 | +//! emits is a typed access against one of these two fields; the raw |
| 29 | +//! byte-buffer copies (`memory.copy`) and the allocator's own |
| 30 | +//! bump-pointer bookkeeping at `mem[0]` / `mem[8]` are NOT typed |
| 31 | +//! accesses and are deliberately unlisted (proposal 0002 §"Producer |
| 32 | +//! obligations" ¶5). |
| 33 | +
|
| 34 | +pub const REGIONS_SECTION_NAME: &str = "typedwasm.regions"; |
| 35 | +pub const ACCESS_SITES_SECTION_NAME: &str = "typedwasm.access-sites"; |
| 36 | + |
| 37 | +pub const REGIONS_VERSION: u16 = 1; |
| 38 | +pub const ACCESS_SITES_VERSION: u16 = 1; |
| 39 | + |
| 40 | +/// Region index of `ephapax.string` in the v1 table. |
| 41 | +pub const REGION_STRING: u32 = 0; |
| 42 | +/// Field indices within `ephapax.string`. |
| 43 | +pub const FIELD_PTR: u32 = 0; |
| 44 | +pub const FIELD_LEN: u32 = 1; |
| 45 | + |
| 46 | +/// Wire codepoints from proposal 0001 (kind / wasm_ty tables). |
| 47 | +const KIND_SCALAR: u8 = 0; |
| 48 | +const WASM_TY_U32: u8 = 2; |
| 49 | +const NO_TARGET_REGION: u32 = 0xFFFF_FFFF; |
| 50 | +const NON_NULL: u8 = 0; |
| 51 | + |
| 52 | +/// One `typedwasm.access-sites` entry: the producer's assertion that |
| 53 | +/// the instruction at `byte_offset` within function `func_idx`'s body |
| 54 | +/// is a typed access against `(region_id, field_id)`. |
| 55 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 56 | +pub struct AccessSite { |
| 57 | + /// Index in the module's function index space (imports included — |
| 58 | + /// the same space `call` immediates and `typedwasm.ownership` |
| 59 | + /// entries use). |
| 60 | + pub func_idx: u32, |
| 61 | + /// Byte offset of the access opcode's first byte within the |
| 62 | + /// function body (counting from the byte after the body's size |
| 63 | + /// prefix, i.e. the start of the locals vector). |
| 64 | + pub byte_offset: u32, |
| 65 | + pub region_id: u32, |
| 66 | + pub field_id: u32, |
| 67 | +} |
| 68 | + |
| 69 | +fn write_leb128_u32(out: &mut Vec<u8>, mut v: u32) { |
| 70 | + loop { |
| 71 | + let byte = (v & 0x7f) as u8; |
| 72 | + v >>= 7; |
| 73 | + if v == 0 { |
| 74 | + out.push(byte); |
| 75 | + return; |
| 76 | + } |
| 77 | + out.push(byte | 0x80); |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +fn read_leb128_u32(bytes: &[u8], pos: &mut usize) -> Option<u32> { |
| 82 | + let mut result: u32 = 0; |
| 83 | + let mut shift = 0u32; |
| 84 | + loop { |
| 85 | + let byte = *bytes.get(*pos)?; |
| 86 | + *pos += 1; |
| 87 | + if shift == 28 && (byte & 0x70) != 0 { |
| 88 | + return None; // overflows u32 |
| 89 | + } |
| 90 | + result |= u32::from(byte & 0x7f) << shift; |
| 91 | + if byte & 0x80 == 0 { |
| 92 | + // Reject overlong encodings (proposal 0002: consumers MUST |
| 93 | + // reject non-shortest forms). |
| 94 | + if byte == 0 && shift != 0 { |
| 95 | + return None; |
| 96 | + } |
| 97 | + return Some(result); |
| 98 | + } |
| 99 | + shift += 7; |
| 100 | + if shift > 28 { |
| 101 | + return None; |
| 102 | + } |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +fn write_str(out: &mut Vec<u8>, s: &str) { |
| 107 | + out.extend_from_slice(&(s.len() as u32).to_le_bytes()); |
| 108 | + out.extend_from_slice(s.as_bytes()); |
| 109 | +} |
| 110 | + |
| 111 | +/// Build the fixed ephapax v1 `typedwasm.regions` payload (proposal |
| 112 | +/// 0001 wire format). |
| 113 | +pub fn build_regions_section_payload() -> Vec<u8> { |
| 114 | + let mut out = Vec::new(); |
| 115 | + out.extend_from_slice(®IONS_VERSION.to_le_bytes()); |
| 116 | + out.extend_from_slice(&1u32.to_le_bytes()); // region_count |
| 117 | + write_str(&mut out, "ephapax.string"); |
| 118 | + out.extend_from_slice(&2u32.to_le_bytes()); // field_count |
| 119 | + for name in ["ptr", "len"] { |
| 120 | + write_str(&mut out, name); |
| 121 | + out.push(KIND_SCALAR); |
| 122 | + out.push(WASM_TY_U32); |
| 123 | + out.extend_from_slice(&NO_TARGET_REGION.to_le_bytes()); |
| 124 | + out.push(NON_NULL); |
| 125 | + out.extend_from_slice(&1u32.to_le_bytes()); // cardinality |
| 126 | + } |
| 127 | + out.extend_from_slice(&8u32.to_le_bytes()); // region_byte_size |
| 128 | + out |
| 129 | +} |
| 130 | + |
| 131 | +/// Encode entries to the `typedwasm.access-sites` payload (proposal |
| 132 | +/// 0002 wire format — encoding B, LEB128 per field). |
| 133 | +pub fn build_access_sites_payload(entries: &[AccessSite]) -> Vec<u8> { |
| 134 | + let mut out = Vec::new(); |
| 135 | + out.extend_from_slice(&ACCESS_SITES_VERSION.to_le_bytes()); |
| 136 | + write_leb128_u32(&mut out, entries.len() as u32); |
| 137 | + for e in entries { |
| 138 | + write_leb128_u32(&mut out, e.func_idx); |
| 139 | + write_leb128_u32(&mut out, e.byte_offset); |
| 140 | + write_leb128_u32(&mut out, e.region_id); |
| 141 | + write_leb128_u32(&mut out, e.field_id); |
| 142 | + } |
| 143 | + out |
| 144 | +} |
| 145 | + |
| 146 | +/// Parse a `typedwasm.access-sites` payload. Returns `None` on a |
| 147 | +/// malformed payload (wrong version, truncation, overlong LEB128). |
| 148 | +pub fn parse_access_sites_payload(bytes: &[u8]) -> Option<Vec<AccessSite>> { |
| 149 | + if bytes.len() < 2 { |
| 150 | + return None; |
| 151 | + } |
| 152 | + let version = u16::from_le_bytes([bytes[0], bytes[1]]); |
| 153 | + if version != ACCESS_SITES_VERSION { |
| 154 | + return None; |
| 155 | + } |
| 156 | + let mut pos = 2usize; |
| 157 | + let count = read_leb128_u32(bytes, &mut pos)?; |
| 158 | + let mut entries = Vec::with_capacity(count as usize); |
| 159 | + for _ in 0..count { |
| 160 | + entries.push(AccessSite { |
| 161 | + func_idx: read_leb128_u32(bytes, &mut pos)?, |
| 162 | + byte_offset: read_leb128_u32(bytes, &mut pos)?, |
| 163 | + region_id: read_leb128_u32(bytes, &mut pos)?, |
| 164 | + field_id: read_leb128_u32(bytes, &mut pos)?, |
| 165 | + }); |
| 166 | + } |
| 167 | + if pos != bytes.len() { |
| 168 | + return None; // trailing garbage |
| 169 | + } |
| 170 | + Some(entries) |
| 171 | +} |
| 172 | + |
| 173 | +#[cfg(test)] |
| 174 | +mod tests { |
| 175 | + use super::*; |
| 176 | + |
| 177 | + #[test] |
| 178 | + fn access_sites_round_trip() { |
| 179 | + let entries = vec![ |
| 180 | + AccessSite { func_idx: 3, byte_offset: 12, region_id: 0, field_id: 0 }, |
| 181 | + AccessSite { func_idx: 3, byte_offset: 19, region_id: 0, field_id: 1 }, |
| 182 | + AccessSite { func_idx: 300, byte_offset: 70_000, region_id: 0, field_id: 1 }, |
| 183 | + ]; |
| 184 | + let payload = build_access_sites_payload(&entries); |
| 185 | + assert_eq!(parse_access_sites_payload(&payload), Some(entries)); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn access_sites_empty_round_trip() { |
| 190 | + let payload = build_access_sites_payload(&[]); |
| 191 | + assert_eq!(payload, vec![1, 0, 0]); // version u16le + count 0 |
| 192 | + assert_eq!(parse_access_sites_payload(&payload), Some(vec![])); |
| 193 | + } |
| 194 | + |
| 195 | + #[test] |
| 196 | + fn rejects_wrong_version_and_truncation() { |
| 197 | + let mut payload = build_access_sites_payload(&[AccessSite { |
| 198 | + func_idx: 1, |
| 199 | + byte_offset: 2, |
| 200 | + region_id: 0, |
| 201 | + field_id: 1, |
| 202 | + }]); |
| 203 | + assert!(parse_access_sites_payload(&payload[..payload.len() - 1]).is_none()); |
| 204 | + payload[0] = 9; |
| 205 | + assert!(parse_access_sites_payload(&payload).is_none()); |
| 206 | + } |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn rejects_overlong_leb128() { |
| 210 | + // count = 0 encoded overlong as [0x80, 0x00] |
| 211 | + let payload = vec![1, 0, 0x80, 0x00]; |
| 212 | + assert!(parse_access_sites_payload(&payload).is_none()); |
| 213 | + } |
| 214 | + |
| 215 | + #[test] |
| 216 | + fn regions_payload_shape() { |
| 217 | + let p = build_regions_section_payload(); |
| 218 | + assert_eq!(&p[0..2], &1u16.to_le_bytes()); // version |
| 219 | + assert_eq!(&p[2..6], &1u32.to_le_bytes()); // one region |
| 220 | + assert_eq!(&p[6..10], &14u32.to_le_bytes()); // name len |
| 221 | + assert_eq!(&p[10..24], b"ephapax.string"); |
| 222 | + assert_eq!(&p[24..28], &2u32.to_le_bytes()); // two fields |
| 223 | + // trailing region_byte_size == 8 |
| 224 | + assert_eq!(&p[p.len() - 4..], &8u32.to_le_bytes()); |
| 225 | + } |
| 226 | +} |
0 commit comments