///|
/// Facts computed from normalized memory and source metadata.
pub(all) struct ImageStatistics {
format : @model.Format
lowest_address : Int64?
highest_address : Int64?
address_span : Int64
payload_bytes : Int
segment_count : Int
gap_count : Int
gap_bytes : Int64
largest_gap : Int64
entry : @model.EntryPoint?
checksum_status : @model.ChecksumStatus
record_count : Int
record_counts : Array[Int]
warning_count : Int
} derive(Eq, Debug)
///|
/// Inspect without allocating dense bytes or depending on textual formatting.
pub fn inspect_image(image : @model.FirmwareImage) -> ImageStatistics {
let gaps = image.memory.gaps()
let mut gap_bytes = 0L
let mut largest_gap = 0L
for gap in gaps {
gap_bytes += gap.length()
largest_gap = largest_gap.max(gap.length())
}
let mut record_count = 0
for count in image.metadata.record_counts {
record_count += count
}
{
format: image.metadata.source_format,
lowest_address: image.memory.lowest_address(),
highest_address: image.memory.highest_address(),
address_span: match image.memory.bounds() {
Some(range) => range.length()
None => 0L
},
payload_bytes: image.memory.payload_size(),
segment_count: image.memory.segment_count(),
gap_count: gaps.length(),
gap_bytes,
largest_gap,
entry: image.entry,
checksum_status: image.metadata.checksum_status,
record_count,
record_counts: image.metadata.record_counts.copy(),
warning_count: image.warnings.length(),
}
}
///|
/// Execution display retains CS:IP notation when available.
pub fn format_entry(entry : @model.EntryPoint?) -> String {
match entry {
None => "none"
Some(Linear(address)) => @model.hex_address(address)
Some(Segment(cs, ip)) =>
"CS:IP " +
cs.to_string(radix=16).to_upper().pad_start(4, '0') +
":" +
ip.to_string(radix=16).to_upper().pad_start(4, '0')
}
}
///|
/// A transformed image has no claim that original record checksums still apply.
pub fn format_checksum(status : @model.ChecksumStatus) -> String {
match status {
Verified => "valid"
NotApplicable => "not applicable"
Derived => "derived image (no source record checksum)"
}
}
///|
/// Stable human-readable report used by the CLI and examples.
pub fn ImageStatistics::render(self : ImageStatistics) -> String {
let out = StringBuilder()
out.write_string("Format: " + self.format.label() + "\n")
out.write_string(
"Payload: \{self.payload_bytes} bytes\nSegments: \{self.segment_count}\n",
)
match (self.lowest_address, self.highest_address) {
(Some(low), Some(high)) =>
out.write_string(
"Address range: " +
@model.hex_address(low) +
" - " +
@model.hex_address(high) +
"\n",
)
_ => out.write_string("Address range: empty\n")
}
out.write_string(
"Address span: \{self.address_span} bytes\nGaps: \{self.gap_count}\nGap bytes: \{self.gap_bytes}\n",
)
out.write_string("Entry point: " + format_entry(self.entry) + "\n")
out.write_string("Checksum: " + format_checksum(self.checksum_status) + "\n")
out.write_string(
"Record count: \{self.record_count}\nWarnings: \{self.warning_count}",
)
out.to_string()
}
///|
/// Compact deterministic report. Ranges are inclusive in rendered output.
pub fn FirmwareDiff::render(self : FirmwareDiff) -> String {
let out = StringBuilder()
out.write_string(
"Identical bytes: \{self.identical_bytes}\nChanged bytes: \{self.changed_bytes}\nAdded bytes: \{self.added_bytes}\nRemoved bytes: \{self.removed_bytes}\nEntry changed: \{self.entry_changed}\n",
)
for
group in [
("Changed ranges", self.changed),
("Only left", self.only_left),
("Only right", self.only_right),
] {
let (name, ranges) = group
out.write_string(name + ":\n")
if ranges.is_empty() {
out.write_string(" none\n")
}
for range in ranges {
out.write_string(" " + range.render() + "\n")
}
}
out.to_string()
}