use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use crate::MAX_IMAGE_SIZE;
pub const IHEX_BLANK_BYTE: u8 = 0xFF;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct LoadAddress(pub usize);
impl LoadAddress {
pub fn is_zero(&self) -> bool {
self.0 == 0
}
pub fn parse_str(s: &str) -> Result<Self, AddressParseError> {
let trimmed = s.trim();
let value = if let Some(hex) = trimmed.strip_prefix('$') {
usize::from_str_radix(hex, 16)
} else if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
usize::from_str_radix(hex, 16)
} else {
trimmed.parse::<usize>()
};
value
.map(LoadAddress)
.map_err(|_| AddressParseError::new(trimmed))
}
}
impl serde::Serialize for LoadAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&alloc::format!("{:#x}", self.0))
}
}
impl<'de> serde::Deserialize<'de> for LoadAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LoadAddressVisitor;
impl serde::de::Visitor<'_> for LoadAddressVisitor {
type Value = LoadAddress;
fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(
"a load address as a non-negative number, or a decimal / 0x- / $-prefixed hex string",
)
}
fn visit_u64<E>(self, v: u64) -> Result<LoadAddress, E>
where
E: serde::de::Error,
{
usize::try_from(v)
.map(LoadAddress)
.map_err(|_| E::custom("load address out of range"))
}
fn visit_i64<E>(self, v: i64) -> Result<LoadAddress, E>
where
E: serde::de::Error,
{
usize::try_from(v)
.map(LoadAddress)
.map_err(|_| E::custom("load address must be non-negative and in range"))
}
fn visit_str<E>(self, v: &str) -> Result<LoadAddress, E>
where
E: serde::de::Error,
{
LoadAddress::parse_str(v).map_err(E::custom)
}
}
deserializer.deserialize_any(LoadAddressVisitor)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for LoadAddress {
fn schema_name() -> alloc::borrow::Cow<'static, str> {
"LoadAddress".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "Intel HEX load address: a non-negative integer, or a string in decimal or 0x-/$-prefixed hexadecimal.",
"oneOf": [
{ "type": "integer", "minimum": 0 },
{ "type": "string", "pattern": r"^(0[xX]|\$)?[0-9a-fA-F]+$" }
]
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AddressParseError {
input: String,
}
impl AddressParseError {
fn new(input: &str) -> Self {
Self {
input: input.to_owned(),
}
}
}
impl core::fmt::Display for AddressParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"invalid load address '{}': expected a decimal value or hexadecimal prefixed with 0x or $",
self.input
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IhexError {
MissingColon { line: usize },
BadHex { line: usize },
BadLength { line: usize },
BadChecksum {
line: usize,
expected: u8,
actual: u8,
},
UnsupportedRecordType { line: usize, record_type: u8 },
AddressBelowLoad {
line: usize,
address: usize,
load_address: usize,
},
OverlappingData { offset: usize },
ImageTooLarge { size: usize, max: usize },
MissingEof,
NoData,
}
impl core::fmt::Display for IhexError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IhexError::MissingColon { line } => {
write!(f, "line {line}: record does not start with ':'")
}
IhexError::BadHex { line } => {
write!(f, "line {line}: invalid or odd-length hexadecimal")
}
IhexError::BadLength { line } => {
write!(f, "line {line}: record length or byte count is invalid")
}
IhexError::BadChecksum {
line,
expected,
actual,
} => write!(
f,
"line {line}: bad checksum, expected {expected:#04x} but found {actual:#04x}"
),
IhexError::UnsupportedRecordType { line, record_type } => {
write!(f, "line {line}: unsupported record type {record_type:#04x}")
}
IhexError::AddressBelowLoad {
line,
address,
load_address,
} => write!(
f,
"line {line}: address {address:#x} is below the load address {load_address:#x}"
),
IhexError::OverlappingData { offset } => {
write!(f, "overlapping data records write to offset {offset:#x}")
}
IhexError::ImageTooLarge { size, max } => write!(
f,
"the Intel HEX image extends to {size} bytes, beyond the {max}-byte maximum"
),
IhexError::MissingEof => {
write!(f, "missing end-of-file record (':00000001FF')")
}
IhexError::NoData => write!(f, "the Intel HEX file contained no data records"),
}
}
}
struct Record {
address: u16,
record_type: u8,
data: Vec<u8>,
}
pub fn decode_ihex(input: &[u8], load_address: usize) -> Result<Vec<u8>, IhexError> {
let mut image: Vec<u8> = Vec::new();
let mut written: Vec<bool> = Vec::new();
let mut extended_base: usize = 0;
let mut seen_eof = false;
let mut any_data = false;
for (idx, raw_line) in input.split(|&b| b == b'\n').enumerate() {
let line_no = idx + 1;
let line = raw_line.trim_ascii();
if line.is_empty() {
continue;
}
if seen_eof {
break;
}
let record = parse_record(line, line_no)?;
match record.record_type {
0x00 => {
if !record.data.is_empty() {
any_data = true;
}
for (i, &byte) in record.data.iter().enumerate() {
let address = extended_base
.checked_add(record.address as usize)
.and_then(|a| a.checked_add(i))
.ok_or(IhexError::ImageTooLarge {
size: usize::MAX,
max: MAX_IMAGE_SIZE,
})?;
if address < load_address {
return Err(IhexError::AddressBelowLoad {
line: line_no,
address,
load_address,
});
}
let offset = address - load_address;
if offset >= MAX_IMAGE_SIZE {
return Err(IhexError::ImageTooLarge {
size: offset + 1,
max: MAX_IMAGE_SIZE,
});
}
if offset >= image.len() {
image.resize(offset + 1, IHEX_BLANK_BYTE);
written.resize(offset + 1, false);
}
if written[offset] {
return Err(IhexError::OverlappingData { offset });
}
written[offset] = true;
image[offset] = byte;
}
}
0x01 => seen_eof = true,
0x02 => {
if record.data.len() != 2 {
return Err(IhexError::BadLength { line: line_no });
}
let segment = ((record.data[0] as usize) << 8) | (record.data[1] as usize);
extended_base = segment << 4;
}
0x04 => {
if record.data.len() != 2 {
return Err(IhexError::BadLength { line: line_no });
}
let upper = ((record.data[0] as usize) << 8) | (record.data[1] as usize);
extended_base = upper << 16;
}
0x03 | 0x05 => {}
other => {
return Err(IhexError::UnsupportedRecordType {
line: line_no,
record_type: other,
});
}
}
}
if !seen_eof {
return Err(IhexError::MissingEof);
}
if !any_data {
return Err(IhexError::NoData);
}
Ok(image)
}
fn parse_record(line: &[u8], line_no: usize) -> Result<Record, IhexError> {
if line.first() != Some(&b':') {
return Err(IhexError::MissingColon { line: line_no });
}
let hex = &line[1..];
if !hex.len().is_multiple_of(2) {
return Err(IhexError::BadHex { line: line_no });
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let mut i = 0;
while i < hex.len() {
let hi = hex_val(hex[i]).ok_or(IhexError::BadHex { line: line_no })?;
let lo = hex_val(hex[i + 1]).ok_or(IhexError::BadHex { line: line_no })?;
bytes.push((hi << 4) | lo);
i += 2;
}
if bytes.len() < 5 {
return Err(IhexError::BadLength { line: line_no });
}
let count = bytes[0] as usize;
if bytes.len() != count + 5 {
return Err(IhexError::BadLength { line: line_no });
}
let sum = bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
if sum != 0 {
let data_sum = bytes[..bytes.len() - 1]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_add(b));
return Err(IhexError::BadChecksum {
line: line_no,
expected: 0u8.wrapping_sub(data_sum),
actual: bytes[bytes.len() - 1],
});
}
let address = ((bytes[1] as u16) << 8) | (bytes[2] as u16);
let record_type = bytes[3];
let data = bytes[4..4 + count].to_vec();
Ok(Record {
address,
record_type,
data,
})
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
pub fn encode_ihex(data: &[u8], load_address: usize) -> String {
let mut out = String::new();
if !data.is_empty() {
let mut current_upper: Option<u16> = None;
let mut offset = 0;
while offset < data.len() {
let address = load_address + offset;
let chunk_len = (data.len() - offset).min(16);
let upper = ((address >> 16) & 0xFFFF) as u16;
if current_upper != Some(upper) {
push_ela_record(&mut out, upper);
current_upper = Some(upper);
}
push_data_record(&mut out, address as u16, &data[offset..offset + chunk_len]);
offset += chunk_len;
}
}
out.push_str(":00000001FF\r\n");
out
}
fn push_hex8(out: &mut String, byte: u8) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0F) as usize] as char);
}
fn push_data_record(out: &mut String, address: u16, data: &[u8]) {
let byte_count = data.len() as u8;
out.push(':');
push_hex8(out, byte_count);
push_hex8(out, (address >> 8) as u8);
push_hex8(out, address as u8);
push_hex8(out, 0x00); let mut csum = byte_count
.wrapping_add((address >> 8) as u8)
.wrapping_add(address as u8);
for &b in data {
push_hex8(out, b);
csum = csum.wrapping_add(b);
}
push_hex8(out, 0u8.wrapping_sub(csum));
out.push_str("\r\n");
}
fn push_ela_record(out: &mut String, upper: u16) {
out.push_str(":02000004");
push_hex8(out, (upper >> 8) as u8);
push_hex8(out, upper as u8);
let csum = 0x02u8
.wrapping_add(0x04)
.wrapping_add((upper >> 8) as u8)
.wrapping_add(upper as u8);
push_hex8(out, 0u8.wrapping_sub(csum));
out.push_str("\r\n");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_str_accepts_decimal_and_hex_forms() {
assert_eq!(LoadAddress::parse_str("0").unwrap(), LoadAddress(0));
assert_eq!(
LoadAddress::parse_str("57344").unwrap(),
LoadAddress(0xE000)
);
assert_eq!(
LoadAddress::parse_str("0xE000").unwrap(),
LoadAddress(0xE000)
);
assert_eq!(
LoadAddress::parse_str("0Xe000").unwrap(),
LoadAddress(0xE000)
);
assert_eq!(
LoadAddress::parse_str("$E000").unwrap(),
LoadAddress(0xE000)
);
assert_eq!(
LoadAddress::parse_str(" $E000 ").unwrap(),
LoadAddress(0xE000)
);
assert!(LoadAddress::parse_str("").is_err());
assert!(LoadAddress::parse_str("$").is_err());
assert!(LoadAddress::parse_str("0xZZ").is_err());
assert!(LoadAddress::parse_str("nope").is_err());
}
#[test]
fn load_address_serde_round_trips() {
let from_num: LoadAddress = serde_json::from_str("57344").unwrap();
assert_eq!(from_num, LoadAddress(0xE000));
let from_hex: LoadAddress = serde_json::from_str("\"0xE000\"").unwrap();
assert_eq!(from_hex, LoadAddress(0xE000));
let from_dollar: LoadAddress = serde_json::from_str("\"$E000\"").unwrap();
assert_eq!(from_dollar, LoadAddress(0xE000));
assert_eq!(
serde_json::to_string(&LoadAddress(0xE000)).unwrap(),
"\"0xe000\""
);
assert!(serde_json::from_str::<LoadAddress>("-1").is_err());
}
fn data_record(address: u16, data: &[u8]) -> String {
let mut bytes = alloc::vec![data.len() as u8, (address >> 8) as u8, address as u8, 0x00];
bytes.extend_from_slice(data);
let data_sum = bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
bytes.push(0u8.wrapping_sub(data_sum));
let mut line = String::from(":");
for b in bytes {
line.push_str(&alloc::format!("{b:02X}"));
}
line
}
const EOF: &str = ":00000001FF";
#[test]
fn decodes_contiguous_image() {
let hex = alloc::format!("{}\n{}\n", data_record(0, &[0xDE, 0xAD, 0xBE, 0xEF]), EOF);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(out, alloc::vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn fills_internal_gaps_with_blank_byte() {
let hex = alloc::format!(
"{}\n{}\n{}\n",
data_record(0, &[0x11, 0x22]),
data_record(4, &[0x33, 0x44]),
EOF
);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(
out,
alloc::vec![0x11, 0x22, IHEX_BLANK_BYTE, IHEX_BLANK_BYTE, 0x33, 0x44]
);
}
#[test]
fn tolerates_crlf_and_blank_lines() {
let hex = alloc::format!("\r\n{}\r\n\r\n{}\r\n", data_record(0, &[0xAB]), EOF);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(out, alloc::vec![0xAB]);
}
#[test]
fn applies_load_address_offset() {
let hex = alloc::format!("{}\n{}\n", data_record(0xE000, &[0x01, 0x02]), EOF);
let out = decode_ihex(hex.as_bytes(), 0xE000).unwrap();
assert_eq!(out, alloc::vec![0x01, 0x02]);
}
#[test]
fn address_below_load_is_an_error() {
let hex = alloc::format!("{}\n{}\n", data_record(0x00, &[0x01]), EOF);
assert!(matches!(
decode_ihex(hex.as_bytes(), 0x10),
Err(IhexError::AddressBelowLoad { .. })
));
}
#[test]
fn extended_linear_address_reaches_beyond_64k() {
let ela = {
let mut bytes = alloc::vec![0x02u8, 0x00, 0x00, 0x04, 0x00, 0x01];
let sum = bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
bytes.push(0u8.wrapping_sub(sum));
let mut line = String::from(":");
for b in bytes {
line.push_str(&alloc::format!("{b:02X}"));
}
line
};
let hex = alloc::format!("{}\n{}\n{}\n", ela, data_record(0x0000, &[0x99]), EOF);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(out.len(), 0x10001);
assert_eq!(out[0x10000], 0x99);
assert_eq!(out[0], IHEX_BLANK_BYTE);
}
#[test]
fn start_address_records_are_ignored() {
let sla = {
let mut bytes = alloc::vec![0x04u8, 0x00, 0x00, 0x05, 0x00, 0x00, 0x80, 0x00];
let sum = bytes.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
bytes.push(0u8.wrapping_sub(sum));
let mut line = String::from(":");
for b in bytes {
line.push_str(&alloc::format!("{b:02X}"));
}
line
};
let hex = alloc::format!("{}\n{}\n{}\n", sla, data_record(0, &[0x42]), EOF);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(out, alloc::vec![0x42]);
}
#[test]
fn missing_eof_is_an_error() {
let hex = alloc::format!("{}\n", data_record(0, &[0x01]));
assert!(matches!(
decode_ihex(hex.as_bytes(), 0),
Err(IhexError::MissingEof)
));
}
#[test]
fn bytes_after_eof_are_ignored() {
let hex = alloc::format!(
"{}\n{}\n{}\n",
data_record(0, &[0x01]),
EOF,
"this is not a valid record"
);
let out = decode_ihex(hex.as_bytes(), 0).unwrap();
assert_eq!(out, alloc::vec![0x01]);
}
#[test]
fn overlapping_records_are_an_error() {
let hex = alloc::format!(
"{}\n{}\n{}\n",
data_record(0, &[0x01, 0x02]),
data_record(1, &[0x03]),
EOF
);
assert!(matches!(
decode_ihex(hex.as_bytes(), 0),
Err(IhexError::OverlappingData { offset: 1 })
));
}
#[test]
fn bad_checksum_is_an_error() {
let mut line = data_record(0, &[0x01, 0x02]);
line.pop();
line.push('0');
let hex = alloc::format!("{}\n{}\n", line, EOF);
assert!(matches!(
decode_ihex(hex.as_bytes(), 0),
Err(IhexError::BadChecksum { .. })
));
}
#[test]
fn no_data_records_is_an_error() {
assert!(matches!(
decode_ihex(EOF.as_bytes(), 0),
Err(IhexError::NoData)
));
}
#[test]
fn bad_hex_is_an_error() {
let hex = alloc::format!(":10000000ZZ\n{}\n", EOF);
assert!(matches!(
decode_ihex(hex.as_bytes(), 0),
Err(IhexError::BadHex { .. })
));
}
#[test]
fn encode_matches_expected_wire_format() {
let out = encode_ihex(&[0x00, 0x01, 0x02, 0x03], 0);
assert_eq!(
out,
concat!(
":020000040000FA\r\n", ":0400000000010203F6\r\n", ":00000001FF\r\n", )
);
}
#[test]
fn encode_decode_round_trips() {
for len in [1usize, 15, 16, 17, 256, 8192] {
let data: Vec<u8> = (0..len)
.map(|i| (i.wrapping_mul(37) ^ 0x5A) as u8)
.collect();
for la in [0usize, 0x10, 0xE000] {
let hex = encode_ihex(&data, la);
let back = decode_ihex(hex.as_bytes(), la).unwrap();
assert_eq!(back, data, "round-trip failed at len={len}, la={la:#x}");
}
}
}
#[test]
fn encode_emits_extended_linear_across_64k() {
let data = alloc::vec![0xABu8; 0x10001];
let hex = encode_ihex(&data, 0);
assert_eq!(hex.matches(":02000004").count(), 2);
assert_eq!(decode_ihex(hex.as_bytes(), 0).unwrap(), data);
}
#[test]
fn encode_empty_is_just_eof() {
assert_eq!(encode_ihex(&[], 0), ":00000001FF\r\n");
}
}