Skip to main content

nros_platform_api/
boot_config.rs

1// ============================================================================
2// BakedBootConfig — RFC-0045 "Single embedded bake site"
3//
4// Lives in nros-platform-api (no deps, no_std) so that nros-platform's
5// DeployOverlay can hold a `&'static BakedBootConfig` without creating a
6// dependency cycle back through nros-node.
7// ============================================================================
8
9/// 0x4E524243 = ASCII "NRBC". A post-link tool scans for this magic to locate
10/// the struct in a firmware image.
11pub const NROS_BOOT_CONFIG_MAGIC: u32 = 0x4E52_4243;
12
13/// Layout version — lets the resolver reject a mismatched baked struct.
14///
15/// **2** since issue 1050 defect (3) appended `rmw` (bit 4). The bump is what
16/// the field is for: a reader compiled against v1 rejects a v2 struct rather
17/// than reading 32 bytes of `rmw` as whatever followed `namespace` in its own
18/// layout.
19pub const NROS_BOOT_CONFIG_VERSION: u16 = 2;
20
21// `set_flags` bit assignments in `BakedBootConfig`.
22/// Bit 0 — `node_name` field is set.
23pub const BOOT_SET_NODE_NAME: u16 = 1 << 0;
24/// Bit 1 — `locator` field is set.
25pub const BOOT_SET_LOCATOR: u16 = 1 << 1;
26/// Bit 2 — `domain_id` field is set.
27pub const BOOT_SET_DOMAIN: u16 = 1 << 2;
28/// Bit 3 — `namespace` field is set.
29pub const BOOT_SET_NAMESPACE: u16 = 1 << 3;
30/// Bit 4 — `rmw` field is set (issue 1050 defect (3); layout version 2).
31pub const BOOT_SET_RMW: u16 = 1 << 4;
32
33/// Build-time-baked boot config, emitted (in W4b) into the `.nros_boot_config`
34/// linker section by the entry macro / cmake. Fixed-size + pointer-free so a
35/// future post-link tool can patch it in place (RFC-0045). The resolver reads
36/// it on embedded via `BootConfig::from_baked` (in `nros-node`).
37#[repr(C)]
38#[derive(Debug, Clone, Copy)]
39pub struct BakedBootConfig {
40    /// 0x4E524243 = b"NRBC". A post-link tool scans for this to locate the struct.
41    pub magic: u32,
42    /// Layout version (start at 1) — lets the tool/reader reject mismatched layouts.
43    pub version: u16,
44    /// One bit per field that is baked-set (else the reader yields `None` → resolver
45    /// default). bit0 = node_name, bit1 = locator, bit2 = domain_id, bit3 = namespace,
46    /// bit4 = rmw.
47    pub set_flags: u16,
48    /// ROS 2 domain ID (valid only when `BOOT_SET_DOMAIN` bit is set).
49    pub domain_id: u32,
50    /// NUL-padded UTF-8; the trailing NUL bytes are not part of the value.
51    pub node_name: [u8; 64],
52    /// NUL-padded UTF-8 middleware locator; the trailing NUL bytes are not part of
53    /// the value.
54    pub locator: [u8; 96],
55    /// NUL-padded UTF-8 node namespace; the trailing NUL bytes are not part of the
56    /// value.
57    pub namespace: [u8; 64],
58    /// Issue 1050 defect (3) — NUL-padded UTF-8 RMW backend selector (the name
59    /// the registry is looked up by, e.g. `"uorb"`). Appended in layout
60    /// version 2.
61    ///
62    /// 32 bytes because that is the registry's own `BACKEND_NAME_MAX`; a longer
63    /// name could never resolve, so accepting one here would only move the
64    /// failure later.
65    pub rmw: [u8; 32],
66}
67
68/// Copy `s` bytes into a zero-padded `[u8; N]` array at compile time.
69///
70/// A string longer than `N` bytes is a **compile-time** error (const panic).
71/// Never silently truncated.
72const fn pack<const N: usize>(s: &str) -> [u8; N] {
73    let bytes = s.as_bytes();
74    if bytes.len() > N {
75        panic!("BakedBootConfig: string field exceeds its fixed-size buffer");
76    }
77    let mut buf = [0u8; N];
78    let mut i = 0;
79    while i < bytes.len() {
80        buf[i] = bytes[i];
81        i += 1;
82    }
83    buf
84}
85
86impl BakedBootConfig {
87    /// Pack baked fields at compile time.  `None` → field unset (bit clear,
88    /// bytes zeroed).  A string longer than its fixed buffer is a
89    /// **compile-time** error (const panic) — never silently truncated.
90    ///
91    /// `must be const fn` so W4b's entry macro can use it in a `static` initializer.
92    pub const fn new(
93        node_name: Option<&str>,
94        locator: Option<&str>,
95        domain_id: Option<u32>,
96        namespace: Option<&str>,
97    ) -> BakedBootConfig {
98        Self::new_with_rmw(node_name, locator, domain_id, namespace, None)
99    }
100
101    /// [`new`](Self::new) plus the layout-version-2 `rmw` selector — issue 1050
102    /// defect (3).
103    ///
104    /// Kept as a separate constructor rather than a fifth parameter on `new`
105    /// because `new` is called from entry macros in board crates and from
106    /// generated code; a signature change there is a break for every one of
107    /// them, and this field is `None` at all but a handful of call sites.
108    pub const fn new_with_rmw(
109        node_name: Option<&str>,
110        locator: Option<&str>,
111        domain_id: Option<u32>,
112        namespace: Option<&str>,
113        rmw: Option<&str>,
114    ) -> BakedBootConfig {
115        let mut flags: u16 = 0;
116
117        let node_name_bytes: [u8; 64] = match node_name {
118            Some(s) => {
119                flags |= BOOT_SET_NODE_NAME;
120                pack::<64>(s)
121            }
122            None => [0u8; 64],
123        };
124
125        let locator_bytes: [u8; 96] = match locator {
126            Some(s) => {
127                flags |= BOOT_SET_LOCATOR;
128                pack::<96>(s)
129            }
130            None => [0u8; 96],
131        };
132
133        let domain_id_val: u32 = match domain_id {
134            Some(d) => {
135                flags |= BOOT_SET_DOMAIN;
136                d
137            }
138            None => 0,
139        };
140
141        let namespace_bytes: [u8; 64] = match namespace {
142            Some(s) => {
143                flags |= BOOT_SET_NAMESPACE;
144                pack::<64>(s)
145            }
146            None => [0u8; 64],
147        };
148
149        let rmw_bytes: [u8; 32] = match rmw {
150            Some(s) => {
151                flags |= BOOT_SET_RMW;
152                pack::<32>(s)
153            }
154            None => [0u8; 32],
155        };
156
157        BakedBootConfig {
158            magic: NROS_BOOT_CONFIG_MAGIC,
159            version: NROS_BOOT_CONFIG_VERSION,
160            set_flags: flags,
161            domain_id: domain_id_val,
162            node_name: node_name_bytes,
163            locator: locator_bytes,
164            namespace: namespace_bytes,
165            rmw: rmw_bytes,
166        }
167    }
168}
169
170// ============================================================================
171// BakedBootConfig unit tests (no_std-compatible, run under std test runner)
172//
173// These tests verify BakedBootConfig::new / pack directly (struct-field
174// inspection only — no BootConfig::from_baked, which lives in nros-node).
175// Round-trip tests (new → from_baked) live in nros-node/src/executor/types.rs.
176// ============================================================================
177
178#[cfg(test)]
179mod baked_boot_config_tests {
180    use super::*;
181
182    // ── T-BB8: set_flags bit-pattern is exact ─────────────────────────────────
183
184    /// Verify the set_flags bitmask matches the expected bit positions.
185    #[test]
186    fn set_flags_bits_correct() {
187        let baked = BakedBootConfig::new(
188            Some("n"), // bit 0
189            None,
190            Some(0),  // bit 2
191            Some(""), // bit 3
192        );
193        assert_eq!(
194            baked.set_flags,
195            BOOT_SET_NODE_NAME | BOOT_SET_DOMAIN | BOOT_SET_NAMESPACE
196        );
197    }
198
199    // ── T-BB-MAGIC: magic and version are populated ───────────────────────────
200
201    /// `BakedBootConfig::new` must embed the correct magic word and version.
202    #[test]
203    fn magic_and_version_populated() {
204        let baked = BakedBootConfig::new(None, None, None, None);
205        assert_eq!(baked.magic, NROS_BOOT_CONFIG_MAGIC);
206        assert_eq!(baked.version, NROS_BOOT_CONFIG_VERSION);
207    }
208
209    // ── T-BB-PACK: short string is NUL-padded ────────────────────────────────
210
211    /// A node_name shorter than 64 bytes must be stored in the leading bytes,
212    /// with the remaining bytes zeroed (NUL-padded).
213    #[test]
214    fn short_name_is_nul_padded() {
215        let name = "robot";
216        let baked = BakedBootConfig::new(Some(name), None, None, None);
217        assert_eq!(&baked.node_name[..name.len()], name.as_bytes());
218        assert!(baked.node_name[name.len()..].iter().all(|&b| b == 0));
219        assert_eq!(baked.set_flags & BOOT_SET_NODE_NAME, BOOT_SET_NODE_NAME);
220    }
221
222    // ── T-BB-NONE: all-None zeroes bytes and clears flags ────────────────────
223
224    /// When every argument is None the set_flags must be zero, domain_id zero,
225    /// and all byte arrays zeroed.
226    #[test]
227    fn all_none_zeroes_fields() {
228        let baked = BakedBootConfig::new(None, None, None, None);
229        assert_eq!(baked.set_flags, 0);
230        assert_eq!(baked.domain_id, 0);
231        assert!(baked.node_name.iter().all(|&b| b == 0));
232        assert!(baked.locator.iter().all(|&b| b == 0));
233        assert!(baked.namespace.iter().all(|&b| b == 0));
234    }
235
236    // ── Compile-failure comment ───────────────────────────────────────────────
237    // Uncommenting the line below must FAIL to compile because the string
238    // exceeds the 64-byte node_name buffer.  Do NOT uncomment in CI.
239    //
240    // const _: BakedBootConfig = BakedBootConfig::new(
241    //     Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), // 65 A's (> 64-byte buffer)
242    //     None, None, None,
243    // );
244
245    // ── T-LAYOUT: layout-drift guard (mirrors C header static_asserts) ────────
246    //
247    // If `BakedBootConfig` ever changes size or field offsets, this test fails
248    // and forces the C header `include/nros/boot_config.h` to be updated in
249    // lockstep.  The C header carries matching _Static_assert / static_assert
250    // guards on the C/C++ side.
251
252    /// Size and field offsets of `BakedBootConfig` must match the C header's
253    /// documented layout (total 268 bytes, no padding).
254    ///
255    /// 236 -> 268 with layout version 2 (issue 1050 defect (3), `rmw @ 236`).
256    /// This test is the reason that append could not be a one-sided edit: the
257    /// blob has a C mirror (`nros-c/include/nros/boot_config.h`) whose own
258    /// `static_assert`s carry the same numbers, so a change here fails until
259    /// both sides move.
260    #[test]
261    fn baked_boot_config_layout() {
262        use core::mem::{offset_of, size_of};
263        assert_eq!(size_of::<BakedBootConfig>(), 268, "total size must be 268");
264        assert_eq!(offset_of!(BakedBootConfig, magic), 0, "magic @ 0");
265        assert_eq!(offset_of!(BakedBootConfig, version), 4, "version @ 4");
266        assert_eq!(offset_of!(BakedBootConfig, set_flags), 6, "set_flags @ 6");
267        assert_eq!(offset_of!(BakedBootConfig, domain_id), 8, "domain_id @ 8");
268        assert_eq!(offset_of!(BakedBootConfig, node_name), 12, "node_name @ 12");
269        assert_eq!(offset_of!(BakedBootConfig, locator), 76, "locator @ 76");
270        assert_eq!(
271            offset_of!(BakedBootConfig, namespace),
272            172,
273            "namespace @ 172"
274        );
275        assert_eq!(offset_of!(BakedBootConfig, rmw), 236, "rmw @ 236");
276    }
277}