///|
/// Record length is configurable; output uses extended linear addressing.
pub(all) struct WriterOptions {
data_length : Int
uppercase : Bool
line_ending : @codec.LineEnding
max_output_length : Int
} derive(Eq, Debug)
///|
/// Standard 16-byte uppercase LF output with a bounded text allocation.
pub fn WriterOptions::default() -> WriterOptions {
{
data_length: 16,
uppercase: true,
line_ending: LF,
max_output_length: 256 * 1024 * 1024,
}
}
///|
/// Encode one validated record and compute its two's complement checksum.
pub fn Record::encode(self : Record, uppercase? : Bool = true) -> String {
let body = [
self.data.length().to_byte(),
(self.address >> 8).to_byte(),
(self.address & 255).to_byte(),
self.kind.to_byte(),
]
for b in self.data {
body.push(b)
}
let bytes = Bytes::from_array(body)
":" +
@codec.encode_hex(bytes, uppercase~) +
@codec.encode_hex(Bytes::from_array([compute_checksum(bytes)]), uppercase~)
}
///|
/// Walk records without allocating the full text document. Callback failures stop
/// emission; streaming clients are responsible for staging partially written IO.
pub fn for_each_record(
image : @model.FirmwareImage,
visit : (Record) -> Unit raise @model.FirmwareError,
data_length? : Int = 16,
) -> Unit raise @model.FirmwareError {
if data_length < 1 || data_length > 255 {
raise @model.FirmwareError(
@model.diagnostic(
InvalidOption,
"Intel HEX record data length must be 1..255",
),
)
}
if image.entry is Some(entry) {
ignore(entry.address())
}
let mut upper = 0L
for segment in image.memory.segments() {
let mut offset = 0
while offset < segment.data.length() {
let absolute = segment.start + offset.to_int64()
let next_upper = absolute >> 16
if next_upper != upper {
visit(Record::new(4, 0, @codec.address_bytes(next_upper, 2)))
upper = next_upper
}
let low = (absolute & 65535L).to_int()
// Split at the 64 KiB bank edge even when data_length permits more data.
let length = data_length
.min(segment.data.length() - offset)
.min(65536 - low)
visit(
Record::new(0, low, segment.data[offset:offset + length].to_owned()),
)
offset += length
}
}
match image.entry {
Some(Linear(address)) =>
visit(Record::new(5, 0, @codec.address_bytes(address, 4)))
Some(Segment(cs, ip)) =>
visit(
Record::new(
3,
0,
@codec.address_bytes(cs.to_int64(), 2) +
@codec.address_bytes(ip.to_int64(), 2),
),
)
None => ()
}
visit(Record::new(1, 0, b""))
}
///|
/// Write a complete deterministic Intel HEX file, preserving execution metadata.
pub fn encode(
image : @model.FirmwareImage,
options? : WriterOptions = WriterOptions::default(),
) -> String raise @model.FirmwareError {
let output = @codec.TextOutput::new(
options.line_ending,
options.max_output_length,
)
for_each_record(
image,
record => output.write_line(record.encode(uppercase=options.uppercase)),
data_length=options.data_length,
)
output.finish()
}