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}