Skip to main content

agent_sim/load/
mod.rs

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, &region.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}