1pub mod resolve;
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::path::Path;
6use thiserror::Error;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct LoadSpec {
10 pub libpath: String,
11 pub env_tag: Option<String>,
12 #[serde(default)]
13 pub flash: Vec<ResolvedFlashRegion>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct ResolvedFlashRegion {
18 pub base_addr: u32,
19 pub data: Vec<u8>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FlashFormat {
24 IntelHex,
25 Srec,
26 Binary,
27}
28
29impl FlashFormat {
30 pub fn parse(raw: &str) -> Result<Self, FlashParseError> {
31 match raw.trim().to_ascii_lowercase().as_str() {
32 "hex" | "ihex" | "intel-hex" | "intel_hex" => Ok(Self::IntelHex),
33 "srec" | "s-record" | "s_record" | "s19" | "s28" | "s37" | "mot" => Ok(Self::Srec),
34 "bin" | "binary" => Ok(Self::Binary),
35 other => Err(FlashParseError::UnsupportedFormat(other.to_string())),
36 }
37 }
38
39 pub fn infer(path: &Path, explicit: Option<&str>) -> Result<Self, FlashParseError> {
40 if let Some(raw) = explicit {
41 return Self::parse(raw);
42 }
43
44 let ext = path
45 .extension()
46 .and_then(|value| value.to_str())
47 .ok_or_else(|| FlashParseError::UnsupportedFormat(path.display().to_string()))?;
48 Self::parse(ext)
49 }
50}
51
52#[derive(Debug, Error)]
53pub enum FlashParseError {
54 #[error("unsupported flash format '{0}'")]
55 UnsupportedFormat(String),
56 #[error("invalid flash address '{0}'")]
57 InvalidAddress(String),
58 #[error("raw binary flash input requires an explicit base address")]
59 MissingBinaryBase,
60 #[error("flash input exceeds 32-bit address space at 0x{base_addr:08X} (+{len} bytes)")]
61 AddressOverflow { base_addr: u32, len: usize },
62 #[error("invalid Intel HEX line {line}: {message}")]
63 InvalidIntelHex { line: usize, message: String },
64 #[error("invalid S-record line {line}: {message}")]
65 InvalidSrec { line: usize, message: String },
66 #[error("failed to read flash file '{path}': {message}")]
67 FileRead { path: String, message: String },
68 #[error("load spec '{path}': {message}")]
69 LoadSpec { path: String, message: String },
70}
71
72pub fn parse_address(raw: &str) -> Result<u32, FlashParseError> {
73 let trimmed = raw.trim();
74 if let Some(hex) = trimmed
75 .strip_prefix("0x")
76 .or_else(|| trimmed.strip_prefix("0X"))
77 {
78 u32::from_str_radix(hex, 16).map_err(|_| FlashParseError::InvalidAddress(raw.to_string()))
79 } else {
80 trimmed
81 .parse::<u32>()
82 .map_err(|_| FlashParseError::InvalidAddress(raw.to_string()))
83 }
84}
85
86pub fn parse_raw_binary(
87 bytes: &[u8],
88 base_addr: u32,
89) -> Result<ResolvedFlashRegion, FlashParseError> {
90 ensure_address_range(base_addr, bytes.len())?;
91 Ok(ResolvedFlashRegion {
92 base_addr,
93 data: bytes.to_vec(),
94 })
95}
96
97pub fn parse_intel_hex(content: &str) -> Result<Vec<ResolvedFlashRegion>, FlashParseError> {
98 let mut upper_addr = 0_u32;
99 let mut memory = FlashMemory::default();
100
101 for (idx, raw_line) in content.lines().enumerate() {
102 let line_no = idx + 1;
103 let line = raw_line.trim();
104 if line.is_empty() {
105 continue;
106 }
107 let payload = line
108 .strip_prefix(':')
109 .ok_or_else(|| FlashParseError::InvalidIntelHex {
110 line: line_no,
111 message: "record must start with ':'".to_string(),
112 })?;
113 if payload.len() < 10 || payload.len() % 2 != 0 {
114 return Err(FlashParseError::InvalidIntelHex {
115 line: line_no,
116 message: "record has invalid hex length".to_string(),
117 });
118 }
119
120 let bytes =
121 decode_hex_bytes(payload).map_err(|message| FlashParseError::InvalidIntelHex {
122 line: line_no,
123 message,
124 })?;
125 let byte_count = usize::from(bytes[0]);
126 if bytes.len() != byte_count + 5 {
127 return Err(FlashParseError::InvalidIntelHex {
128 line: line_no,
129 message: format!(
130 "record length mismatch: byte_count={} actual_data_bytes={}",
131 byte_count,
132 bytes.len().saturating_sub(5)
133 ),
134 });
135 }
136
137 let checksum = bytes
138 .iter()
139 .fold(0_u8, |acc, value| acc.wrapping_add(*value));
140 if checksum != 0 {
141 return Err(FlashParseError::InvalidIntelHex {
142 line: line_no,
143 message: "checksum mismatch".to_string(),
144 });
145 }
146
147 let address = u16::from(bytes[1]) << 8 | u16::from(bytes[2]);
148 let record_type = bytes[3];
149 let data = &bytes[4..4 + byte_count];
150 match record_type {
151 0x00 => {
152 let base_addr = upper_addr.checked_add(u32::from(address)).ok_or(
153 FlashParseError::AddressOverflow {
154 base_addr: upper_addr,
155 len: usize::from(address),
156 },
157 )?;
158 memory.write(base_addr, data)?;
159 }
160 0x01 => break,
161 0x02 => {
162 if data.len() != 2 {
163 return Err(FlashParseError::InvalidIntelHex {
164 line: line_no,
165 message: "extended segment address record must contain 2 data bytes"
166 .to_string(),
167 });
168 }
169 upper_addr = ((u32::from(data[0]) << 8) | u32::from(data[1])) << 4;
170 }
171 0x04 => {
172 if data.len() != 2 {
173 return Err(FlashParseError::InvalidIntelHex {
174 line: line_no,
175 message: "extended linear address record must contain 2 data bytes"
176 .to_string(),
177 });
178 }
179 upper_addr = ((u32::from(data[0]) << 8) | u32::from(data[1])) << 16;
180 }
181 0x03 | 0x05 => {}
182 other => {
183 return Err(FlashParseError::InvalidIntelHex {
184 line: line_no,
185 message: format!("unsupported record type 0x{other:02X}"),
186 });
187 }
188 }
189 }
190
191 Ok(memory.into_regions())
192}
193
194pub fn parse_srec(content: &str) -> Result<Vec<ResolvedFlashRegion>, FlashParseError> {
195 let mut memory = FlashMemory::default();
196
197 for (idx, raw_line) in content.lines().enumerate() {
198 let line_no = idx + 1;
199 let line = raw_line.trim();
200 if line.is_empty() {
201 continue;
202 }
203 if !line.starts_with('S') || line.len() < 4 {
204 return Err(FlashParseError::InvalidSrec {
205 line: line_no,
206 message: "record must start with 'S' and include a type/length".to_string(),
207 });
208 }
209
210 let record_type = line.as_bytes()[1] as char;
211 let rest = &line[2..];
212 if rest.len() % 2 != 0 {
213 return Err(FlashParseError::InvalidSrec {
214 line: line_no,
215 message: "record hex payload must contain an even number of digits".to_string(),
216 });
217 }
218 let bytes = decode_hex_bytes(rest).map_err(|message| FlashParseError::InvalidSrec {
219 line: line_no,
220 message,
221 })?;
222 if bytes.is_empty() {
223 return Err(FlashParseError::InvalidSrec {
224 line: line_no,
225 message: "record is missing the byte-count field".to_string(),
226 });
227 }
228
229 let declared_count = usize::from(bytes[0]);
230 if declared_count != bytes.len().saturating_sub(1) {
231 return Err(FlashParseError::InvalidSrec {
232 line: line_no,
233 message: format!(
234 "record length mismatch: byte_count={} actual={}",
235 declared_count,
236 bytes.len().saturating_sub(1)
237 ),
238 });
239 }
240
241 let checksum = bytes
242 .iter()
243 .fold(0_u8, |acc, value| acc.wrapping_add(*value));
244 if checksum != 0xFF {
245 return Err(FlashParseError::InvalidSrec {
246 line: line_no,
247 message: "checksum mismatch".to_string(),
248 });
249 }
250
251 let addr_len = match record_type {
252 '0' | '1' | '5' | '9' => 2,
253 '2' | '6' | '8' => 3,
254 '3' | '7' => 4,
255 other => {
256 return Err(FlashParseError::InvalidSrec {
257 line: line_no,
258 message: format!("unsupported record type 'S{other}'"),
259 });
260 }
261 };
262 if bytes.len() < addr_len + 2 {
263 return Err(FlashParseError::InvalidSrec {
264 line: line_no,
265 message: "record is too short for its address size".to_string(),
266 });
267 }
268
269 if matches!(record_type, '1' | '2' | '3') {
270 let addr = bytes[1..1 + addr_len]
271 .iter()
272 .fold(0_u32, |acc, value| (acc << 8) | u32::from(*value));
273 let data = &bytes[1 + addr_len..bytes.len() - 1];
274 memory.write(addr, data)?;
275 }
276 }
277
278 Ok(memory.into_regions())
279}
280
281pub fn resolve_flash_file(
282 path: &Path,
283 format: Option<&str>,
284 base_addr: Option<u32>,
285) -> Result<Vec<ResolvedFlashRegion>, FlashParseError> {
286 let flash_format = FlashFormat::infer(path, format)?;
287 match flash_format {
288 FlashFormat::IntelHex => {
289 let content =
290 std::fs::read_to_string(path).map_err(|err| FlashParseError::FileRead {
291 path: path.display().to_string(),
292 message: err.to_string(),
293 })?;
294 parse_intel_hex(&content)
295 }
296 FlashFormat::Srec => {
297 let content =
298 std::fs::read_to_string(path).map_err(|err| FlashParseError::FileRead {
299 path: path.display().to_string(),
300 message: err.to_string(),
301 })?;
302 parse_srec(&content)
303 }
304 FlashFormat::Binary => {
305 let bytes = std::fs::read(path).map_err(|err| FlashParseError::FileRead {
306 path: path.display().to_string(),
307 message: err.to_string(),
308 })?;
309 let region =
310 parse_raw_binary(&bytes, base_addr.ok_or(FlashParseError::MissingBinaryBase)?)?;
311 Ok(vec![region])
312 }
313 }
314}
315
316pub fn merge_regions(
317 regions: &[ResolvedFlashRegion],
318) -> Result<Vec<ResolvedFlashRegion>, FlashParseError> {
319 let mut memory = FlashMemory::default();
320 for region in regions {
321 memory.write(region.base_addr, ®ion.data)?;
322 }
323 Ok(memory.into_regions())
324}
325
326pub fn read_load_spec(path: &Path) -> Result<LoadSpec, FlashParseError> {
327 let content = std::fs::read_to_string(path).map_err(|err| FlashParseError::LoadSpec {
328 path: path.display().to_string(),
329 message: err.to_string(),
330 })?;
331 serde_json::from_str(&content).map_err(|err| FlashParseError::LoadSpec {
332 path: path.display().to_string(),
333 message: format!("invalid load spec json: {err}"),
334 })
335}
336
337pub fn write_load_spec(path: &Path, spec: &LoadSpec) -> Result<(), FlashParseError> {
338 let content = serde_json::to_string(spec).map_err(|err| FlashParseError::LoadSpec {
339 path: path.display().to_string(),
340 message: format!("failed to serialize load spec: {err}"),
341 })?;
342 std::fs::write(path, content).map_err(|err| FlashParseError::LoadSpec {
343 path: path.display().to_string(),
344 message: err.to_string(),
345 })
346}
347
348pub fn encode_inline_u32(value: u32) -> Vec<u8> {
349 value.to_le_bytes().to_vec()
350}
351
352pub fn encode_inline_i32(value: i32) -> Vec<u8> {
353 value.to_le_bytes().to_vec()
354}
355
356pub fn encode_inline_f32(value: f32) -> Vec<u8> {
357 value.to_le_bytes().to_vec()
358}
359
360pub fn encode_inline_bool(value: bool) -> Vec<u8> {
361 vec![u8::from(value)]
362}
363
364#[derive(Debug, Default)]
365struct FlashMemory {
366 bytes: BTreeMap<u32, u8>,
367}
368
369impl FlashMemory {
370 fn write(&mut self, base_addr: u32, data: &[u8]) -> Result<(), FlashParseError> {
371 ensure_address_range(base_addr, data.len())?;
372 for (offset, value) in data.iter().enumerate() {
373 self.bytes.insert(base_addr + offset as u32, *value);
374 }
375 Ok(())
376 }
377
378 fn into_regions(self) -> Vec<ResolvedFlashRegion> {
379 let mut regions = Vec::new();
380 let mut current_base = None;
381 let mut previous_addr = 0_u32;
382 let mut current = Vec::new();
383
384 for (addr, value) in self.bytes {
385 match current_base {
386 None => {
387 current_base = Some(addr);
388 previous_addr = addr;
389 current.push(value);
390 }
391 Some(_) if addr == previous_addr.saturating_add(1) => {
392 previous_addr = addr;
393 current.push(value);
394 }
395 Some(base) => {
396 regions.push(ResolvedFlashRegion {
397 base_addr: base,
398 data: std::mem::take(&mut current),
399 });
400 current_base = Some(addr);
401 previous_addr = addr;
402 current.push(value);
403 }
404 }
405 }
406
407 if let Some(base_addr) = current_base {
408 regions.push(ResolvedFlashRegion {
409 base_addr,
410 data: current,
411 });
412 }
413 regions
414 }
415}
416
417fn ensure_address_range(base_addr: u32, len: usize) -> Result<(), FlashParseError> {
418 if len == 0 {
419 return Ok(());
420 }
421 let last_addr = u64::from(base_addr) + len as u64 - 1;
422 if last_addr > u64::from(u32::MAX) {
423 return Err(FlashParseError::AddressOverflow { base_addr, len });
424 }
425 Ok(())
426}
427
428fn decode_hex_bytes(raw: &str) -> Result<Vec<u8>, String> {
429 if !raw.len().is_multiple_of(2) {
430 return Err("hex payload must contain an even number of digits".to_string());
431 }
432 let mut out = Vec::with_capacity(raw.len() / 2);
433 let mut idx = 0;
434 while idx < raw.len() {
435 let pair = &raw[idx..idx + 2];
436 let value =
437 u8::from_str_radix(pair, 16).map_err(|_| format!("invalid hex byte '{pair}'"))?;
438 out.push(value);
439 idx += 2;
440 }
441 Ok(out)
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn parse_address_accepts_hex_and_decimal() {
450 assert_eq!(
451 parse_address("0x08000000").expect("hex address should parse"),
452 0x0800_0000
453 );
454 assert_eq!(
455 parse_address("4096").expect("decimal address should parse"),
456 4096
457 );
458 }
459
460 #[test]
461 fn parse_intel_hex_merges_records_and_validates_checksum() {
462 let content = concat!(
463 ":020000040800F2\n",
464 ":0400000001020304F2\n",
465 ":00000001FF\n"
466 );
467 let regions = parse_intel_hex(content).expect("valid ihex should parse");
468 assert_eq!(
469 regions,
470 vec![ResolvedFlashRegion {
471 base_addr: 0x0800_0000,
472 data: vec![1, 2, 3, 4],
473 }]
474 );
475
476 let err = parse_intel_hex(":0400000001020304F3\n").expect_err("bad checksum must fail");
477 assert!(matches!(err, FlashParseError::InvalidIntelHex { .. }));
478 }
479
480 #[test]
481 fn parse_srec_reads_data_records_and_validates_checksum() {
482 let content = concat!("S00600004844521B\n", "S107123401020304A8\n", "S9030000FC\n");
483 let regions = parse_srec(content).expect("valid srec should parse");
484 assert_eq!(
485 regions,
486 vec![ResolvedFlashRegion {
487 base_addr: 0x1234,
488 data: vec![1, 2, 3, 4],
489 }]
490 );
491
492 let err = parse_srec("S107123401020304A9\n").expect_err("bad checksum must fail");
493 assert!(matches!(err, FlashParseError::InvalidSrec { .. }));
494 }
495
496 #[test]
497 fn flash_memory_last_write_wins_and_regions_compact() {
498 let mut memory = FlashMemory::default();
499 memory
500 .write(0x1000, &[1, 2, 3])
501 .expect("first write should succeed");
502 memory
503 .write(0x1001, &[9])
504 .expect("overlapping write should succeed");
505 memory
506 .write(0x2000, &[7])
507 .expect("disjoint write should succeed");
508 assert_eq!(
509 memory.into_regions(),
510 vec![
511 ResolvedFlashRegion {
512 base_addr: 0x1000,
513 data: vec![1, 9, 3],
514 },
515 ResolvedFlashRegion {
516 base_addr: 0x2000,
517 data: vec![7],
518 },
519 ]
520 );
521 }
522
523 #[test]
524 fn parse_raw_binary_requires_32bit_address_space() {
525 let region = parse_raw_binary(&[0xAA, 0xBB], 0x0800_0000).expect("binary should parse");
526 assert_eq!(region.base_addr, 0x0800_0000);
527 assert_eq!(region.data, vec![0xAA, 0xBB]);
528
529 let err = parse_raw_binary(&[0; 2], u32::MAX).expect_err("overflow must fail");
530 assert!(matches!(err, FlashParseError::AddressOverflow { .. }));
531 }
532
533 #[test]
534 fn inline_values_encode_little_endian() {
535 assert_eq!(encode_inline_u32(0x1234_5678), vec![0x78, 0x56, 0x34, 0x12]);
536 assert_eq!(encode_inline_i32(-2), (-2_i32).to_le_bytes().to_vec());
537 assert_eq!(encode_inline_f32(3.5), 3.5_f32.to_le_bytes().to_vec());
538 assert_eq!(encode_inline_bool(true), vec![1]);
539 assert_eq!(encode_inline_bool(false), vec![0]);
540 }
541
542 #[test]
543 fn read_load_spec_reports_load_spec_context() {
544 let temp = tempfile::NamedTempFile::new().expect("temp file should be creatable");
545 std::fs::write(temp.path(), "{ not-json }").expect("temp file should be writable");
546
547 let err = read_load_spec(temp.path()).expect_err("invalid json must fail");
548
549 assert!(matches!(err, FlashParseError::LoadSpec { .. }));
550 let message = err.to_string();
551 assert!(message.contains("load spec"), "unexpected error: {message}");
552 assert!(
553 !message.contains("flash file"),
554 "error should not refer to flash files: {message}"
555 );
556 }
557
558 #[test]
559 fn write_load_spec_reports_load_spec_context() {
560 let temp = tempfile::tempdir().expect("temp dir should be creatable");
561 let missing_parent = temp.path().join("missing").join("spec.json");
562 let spec = LoadSpec {
563 libpath: "libsim.so".to_string(),
564 env_tag: Some("demo".to_string()),
565 flash: Vec::new(),
566 };
567
568 let err = write_load_spec(&missing_parent, &spec).expect_err("missing parent must fail");
569
570 assert!(matches!(err, FlashParseError::LoadSpec { .. }));
571 let message = err.to_string();
572 assert!(message.contains("load spec"), "unexpected error: {message}");
573 assert!(
574 !message.contains("flash file"),
575 "error should not refer to flash files: {message}"
576 );
577 }
578}