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.
14pub const NROS_BOOT_CONFIG_VERSION: u16 = 1;
15
16// `set_flags` bit assignments in `BakedBootConfig`.
17/// Bit 0 — `node_name` field is set.
18pub const BOOT_SET_NODE_NAME: u16 = 1 << 0;
19/// Bit 1 — `locator` field is set.
20pub const BOOT_SET_LOCATOR: u16 = 1 << 1;
21/// Bit 2 — `domain_id` field is set.
22pub const BOOT_SET_DOMAIN: u16 = 1 << 2;
23/// Bit 3 — `namespace` field is set.
24pub const BOOT_SET_NAMESPACE: u16 = 1 << 3;
25
26/// Build-time-baked boot config, emitted (in W4b) into the `.nros_boot_config`
27/// linker section by the entry macro / cmake. Fixed-size + pointer-free so a
28/// future post-link tool can patch it in place (RFC-0045). The resolver reads
29/// it on embedded via `BootConfig::from_baked` (in `nros-node`).
30#[repr(C)]
31#[derive(Debug, Clone, Copy)]
32pub struct BakedBootConfig {
33    /// 0x4E524243 = b"NRBC". A post-link tool scans for this to locate the struct.
34    pub magic: u32,
35    /// Layout version (start at 1) — lets the tool/reader reject mismatched layouts.
36    pub version: u16,
37    /// One bit per field that is baked-set (else the reader yields `None` → resolver
38    /// default). bit0 = node_name, bit1 = locator, bit2 = domain_id, bit3 = namespace.
39    pub set_flags: u16,
40    /// ROS 2 domain ID (valid only when `BOOT_SET_DOMAIN` bit is set).
41    pub domain_id: u32,
42    /// NUL-padded UTF-8; the trailing NUL bytes are not part of the value.
43    pub node_name: [u8; 64],
44    /// NUL-padded UTF-8 middleware locator; the trailing NUL bytes are not part of
45    /// the value.
46    pub locator: [u8; 96],
47    /// NUL-padded UTF-8 node namespace; the trailing NUL bytes are not part of the
48    /// value.
49    pub namespace: [u8; 64],
50}
51
52/// Copy `s` bytes into a zero-padded `[u8; N]` array at compile time.
53///
54/// A string longer than `N` bytes is a **compile-time** error (const panic).
55/// Never silently truncated.
56const fn pack<const N: usize>(s: &str) -> [u8; N] {
57    let bytes = s.as_bytes();
58    if bytes.len() > N {
59        panic!("BakedBootConfig: string field exceeds its fixed-size buffer");
60    }
61    let mut buf = [0u8; N];
62    let mut i = 0;
63    while i < bytes.len() {
64        buf[i] = bytes[i];
65        i += 1;
66    }
67    buf
68}
69
70impl BakedBootConfig {
71    /// Pack baked fields at compile time.  `None` → field unset (bit clear,
72    /// bytes zeroed).  A string longer than its fixed buffer is a
73    /// **compile-time** error (const panic) — never silently truncated.
74    ///
75    /// `must be const fn` so W4b's entry macro can use it in a `static` initializer.
76    pub const fn new(
77        node_name: Option<&str>,
78        locator: Option<&str>,
79        domain_id: Option<u32>,
80        namespace: Option<&str>,
81    ) -> BakedBootConfig {
82        let mut flags: u16 = 0;
83
84        let node_name_bytes: [u8; 64] = match node_name {
85            Some(s) => {
86                flags |= BOOT_SET_NODE_NAME;
87                pack::<64>(s)
88            }
89            None => [0u8; 64],
90        };
91
92        let locator_bytes: [u8; 96] = match locator {
93            Some(s) => {
94                flags |= BOOT_SET_LOCATOR;
95                pack::<96>(s)
96            }
97            None => [0u8; 96],
98        };
99
100        let domain_id_val: u32 = match domain_id {
101            Some(d) => {
102                flags |= BOOT_SET_DOMAIN;
103                d
104            }
105            None => 0,
106        };
107
108        let namespace_bytes: [u8; 64] = match namespace {
109            Some(s) => {
110                flags |= BOOT_SET_NAMESPACE;
111                pack::<64>(s)
112            }
113            None => [0u8; 64],
114        };
115
116        BakedBootConfig {
117            magic: NROS_BOOT_CONFIG_MAGIC,
118            version: NROS_BOOT_CONFIG_VERSION,
119            set_flags: flags,
120            domain_id: domain_id_val,
121            node_name: node_name_bytes,
122            locator: locator_bytes,
123            namespace: namespace_bytes,
124        }
125    }
126}
127
128// ============================================================================
129// BakedBootConfig unit tests (no_std-compatible, run under std test runner)
130//
131// These tests verify BakedBootConfig::new / pack directly (struct-field
132// inspection only — no BootConfig::from_baked, which lives in nros-node).
133// Round-trip tests (new → from_baked) live in nros-node/src/executor/types.rs.
134// ============================================================================
135
136#[cfg(test)]
137mod baked_boot_config_tests {
138    use super::*;
139
140    // ── T-BB8: set_flags bit-pattern is exact ─────────────────────────────────
141
142    /// Verify the set_flags bitmask matches the expected bit positions.
143    #[test]
144    fn set_flags_bits_correct() {
145        let baked = BakedBootConfig::new(
146            Some("n"), // bit 0
147            None,
148            Some(0),  // bit 2
149            Some(""), // bit 3
150        );
151        assert_eq!(
152            baked.set_flags,
153            BOOT_SET_NODE_NAME | BOOT_SET_DOMAIN | BOOT_SET_NAMESPACE
154        );
155    }
156
157    // ── T-BB-MAGIC: magic and version are populated ───────────────────────────
158
159    /// `BakedBootConfig::new` must embed the correct magic word and version.
160    #[test]
161    fn magic_and_version_populated() {
162        let baked = BakedBootConfig::new(None, None, None, None);
163        assert_eq!(baked.magic, NROS_BOOT_CONFIG_MAGIC);
164        assert_eq!(baked.version, NROS_BOOT_CONFIG_VERSION);
165    }
166
167    // ── T-BB-PACK: short string is NUL-padded ────────────────────────────────
168
169    /// A node_name shorter than 64 bytes must be stored in the leading bytes,
170    /// with the remaining bytes zeroed (NUL-padded).
171    #[test]
172    fn short_name_is_nul_padded() {
173        let name = "robot";
174        let baked = BakedBootConfig::new(Some(name), None, None, None);
175        assert_eq!(&baked.node_name[..name.len()], name.as_bytes());
176        assert!(baked.node_name[name.len()..].iter().all(|&b| b == 0));
177        assert_eq!(baked.set_flags & BOOT_SET_NODE_NAME, BOOT_SET_NODE_NAME);
178    }
179
180    // ── T-BB-NONE: all-None zeroes bytes and clears flags ────────────────────
181
182    /// When every argument is None the set_flags must be zero, domain_id zero,
183    /// and all byte arrays zeroed.
184    #[test]
185    fn all_none_zeroes_fields() {
186        let baked = BakedBootConfig::new(None, None, None, None);
187        assert_eq!(baked.set_flags, 0);
188        assert_eq!(baked.domain_id, 0);
189        assert!(baked.node_name.iter().all(|&b| b == 0));
190        assert!(baked.locator.iter().all(|&b| b == 0));
191        assert!(baked.namespace.iter().all(|&b| b == 0));
192    }
193
194    // ── Compile-failure comment ───────────────────────────────────────────────
195    // Uncommenting the line below must FAIL to compile because the string
196    // exceeds the 64-byte node_name buffer.  Do NOT uncomment in CI.
197    //
198    // const _: BakedBootConfig = BakedBootConfig::new(
199    //     Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), // 65 A's (> 64-byte buffer)
200    //     None, None, None,
201    // );
202
203    // ── T-LAYOUT: layout-drift guard (mirrors C header static_asserts) ────────
204    //
205    // If `BakedBootConfig` ever changes size or field offsets, this test fails
206    // and forces the C header `include/nros/boot_config.h` to be updated in
207    // lockstep.  The C header carries matching _Static_assert / static_assert
208    // guards on the C/C++ side.
209
210    /// Size and field offsets of `BakedBootConfig` must match the C header's
211    /// documented layout (total 236 bytes, no padding).
212    #[test]
213    fn baked_boot_config_layout() {
214        use core::mem::{offset_of, size_of};
215        assert_eq!(size_of::<BakedBootConfig>(), 236, "total size must be 236");
216        assert_eq!(offset_of!(BakedBootConfig, magic), 0, "magic @ 0");
217        assert_eq!(offset_of!(BakedBootConfig, version), 4, "version @ 4");
218        assert_eq!(offset_of!(BakedBootConfig, set_flags), 6, "set_flags @ 6");
219        assert_eq!(offset_of!(BakedBootConfig, domain_id), 8, "domain_id @ 8");
220        assert_eq!(offset_of!(BakedBootConfig, node_name), 12, "node_name @ 12");
221        assert_eq!(offset_of!(BakedBootConfig, locator), 76, "locator @ 76");
222        assert_eq!(
223            offset_of!(BakedBootConfig, namespace),
224            172,
225            "namespace @ 172"
226        );
227    }
228}