| 1 | //! Intel HEX reading and writing. |
| 2 | |
| 3 | use std::collections::BTreeMap; |
| 4 | use std::fmt::Write; |
| 5 | |
| 6 | const RECORD_LEN: usize = 16; |
| 7 | |
| 8 | /// Writes data records for contiguous runs of bytes, then the EOF record. |
| 9 | pub fn write(bytes: impl Iterator<Item = (u16, u8)>) -> String { |
| 10 | let image: BTreeMap<u16, u8> = bytes.collect(); |
| 11 | let mut out = String::new(); |
| 12 | let mut run: Vec<u8> = Vec::new(); |
| 13 | let mut run_start = 0u16; |
| 14 | let mut flush = |start: u16, data: &mut Vec<u8>| { |
| 15 | for (i, chunk) in data.chunks(RECORD_LEN).enumerate() { |
| 16 | record( |
| 17 | &mut out, |
| 18 | start.wrapping_add((i * RECORD_LEN) as u16), |
| 19 | 0x00, |
| 20 | chunk, |
| 21 | ); |
| 22 | } |
| 23 | data.clear(); |
| 24 | }; |
| 25 | for (&addr, &b) in &image { |
| 26 | if !run.is_empty() && run_start.wrapping_add(run.len() as u16) != addr { |
| 27 | flush(run_start, &mut run); |
| 28 | } |
| 29 | if run.is_empty() { |
| 30 | run_start = addr; |
| 31 | } |
| 32 | run.push(b); |
| 33 | } |
| 34 | flush(run_start, &mut run); |
| 35 | record(&mut out, 0, 0x01, &[]); |
| 36 | out |
| 37 | } |
| 38 | |
| 39 | fn record(out: &mut String, addr: u16, kind: u8, data: &[u8]) { |
| 40 | let mut sum = data.len() as u8; |
| 41 | sum = sum |
| 42 | .wrapping_add((addr >> 8) as u8) |
| 43 | .wrapping_add(addr as u8) |
| 44 | .wrapping_add(kind); |
| 45 | let _ = write!(out, ":{:02X}{addr:04X}{kind:02X}", data.len()); |
| 46 | for &b in data { |
| 47 | sum = sum.wrapping_add(b); |
| 48 | let _ = write!(out, "{b:02X}"); |
| 49 | } |
| 50 | let _ = writeln!(out, "{:02X}", sum.wrapping_neg()); |
| 51 | } |
| 52 | |
| 53 | /// Parses Intel HEX into (address, byte) pairs. |
| 54 | pub fn read(text: &str) -> Result<Vec<(u16, u8)>, String> { |
| 55 | let mut out = Vec::new(); |
| 56 | for (n, line) in text.lines().enumerate() { |
| 57 | let line = line.trim(); |
| 58 | if line.is_empty() { |
| 59 | continue; |
| 60 | } |
| 61 | let fail = |m: &str| format!("line {}: {m}", n + 1); |
| 62 | let hex = line |
| 63 | .strip_prefix(':') |
| 64 | .ok_or_else(|| fail("record must start with ':'"))?; |
| 65 | if hex.len() % 2 != 0 || hex.len() < 10 { |
| 66 | return Err(fail("malformed record")); |
| 67 | } |
| 68 | let raw = (0..hex.len()) |
| 69 | .step_by(2) |
| 70 | .map(|i| u8::from_str_radix(&hex[i..i + 2], 16)) |
| 71 | .collect::<Result<Vec<u8>, _>>() |
| 72 | .map_err(|_| fail("invalid hex digit"))?; |
| 73 | let len = raw[0] as usize; |
| 74 | if raw.len() != len + 5 { |
| 75 | return Err(fail("record length mismatch")); |
| 76 | } |
| 77 | if raw.iter().fold(0u8, |a, &b| a.wrapping_add(b)) != 0 { |
| 78 | return Err(fail("checksum mismatch")); |
| 79 | } |
| 80 | let addr = u16::from_be_bytes([raw[1], raw[2]]); |
| 81 | match raw[3] { |
| 82 | 0x00 => out.extend( |
| 83 | raw[4..4 + len] |
| 84 | .iter() |
| 85 | .enumerate() |
| 86 | .map(|(i, &b)| (addr.wrapping_add(i as u16), b)), |
| 87 | ), |
| 88 | 0x01 => break, |
| 89 | // Extended address / start address records don't apply to a 64K 8051. |
| 90 | _ => {} |
| 91 | } |
| 92 | } |
| 93 | Ok(out) |
| 94 | } |
| 95 | |
| 96 | #[cfg(test)] |
| 97 | mod tests { |
| 98 | use super::*; |
| 99 | |
| 100 | #[test] |
| 101 | fn round_trip() { |
| 102 | let bytes: Vec<(u16, u8)> = (0..20u16) |
| 103 | .map(|i| (i, i as u8)) |
| 104 | .chain([(0x100, 0xAA)]) |
| 105 | .collect(); |
| 106 | let text = write(bytes.iter().copied()); |
| 107 | assert!(text.ends_with(":00000001FF\n")); |
| 108 | assert_eq!(read(&text).unwrap(), bytes); |
| 109 | } |
| 110 | |
| 111 | #[test] |
| 112 | fn known_record() { |
| 113 | // MOV A,#55h ; SJMP $ |
| 114 | let text = write([(0, 0x74), (1, 0x55), (2, 0x80), (3, 0xFE)].into_iter()); |
| 115 | assert_eq!(text.lines().next().unwrap(), ":04000000745580FEB5"); |
| 116 | } |
| 117 | } |