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}