///|
/// Validate occupied address ranges, budgets, alignment and execution entry
/// against a board layout. Work is proportional to payload/segment snapshots
/// and region boundaries, never the numeric distance between sparse segments.
/// Diagnostic order is budgets, occupied regions in address order, then entry.
pub fn validate_layout(
image : @model.FirmwareImage,
options : LayoutOptions,
) -> LayoutReport raise @model.FirmwareError {
let regions = checked_regions(options)
let issues : Array[LayoutIssue] = []
let bounds = image.memory.bounds()
let span = match bounds {
Some(range) => range.length()
None => 0L
}
let payload = image.memory.payload_size()
if payload > options.max_payload_bytes {
issues.push({
rule: PayloadBudget,
level: Error,
range: bounds,
region: None,
message: "payload \{payload} exceeds budget \{options.max_payload_bytes} bytes",
})
}
if span > options.max_span_bytes {
issues.push({
rule: SpanBudget,
level: Error,
range: bounds,
region: None,
message: "address span \{span} exceeds budget \{options.max_span_bytes} bytes",
})
}
let mut first_region = 0
for segment in image.memory.segments() {
let range = segment.range()
if range.start % options.segment_start_alignment.to_int64() != 0L {
issues.push({
rule: SegmentStartAlignment,
level: Error,
range: Some(range),
region: None,
message: "segment start must align to \{options.segment_start_alignment} bytes",
})
}
if range.length() % options.segment_length_alignment.to_int64() != 0L {
issues.push({
rule: SegmentLengthAlignment,
level: Error,
range: Some(range),
region: None,
message: "segment length must align to \{options.segment_length_alignment} bytes",
})
}
// Sorted disjoint regions permit a monotonic cursor even for huge gaps.
while first_region < regions.length() &&
regions[first_region].range.end <= range.start {
first_region += 1
}
let mut cursor = range.start
let mut index = first_region
while index < regions.length() && regions[index].range.start < range.end {
let allowed = regions[index].range
if cursor < allowed.start {
outside_issue(
issues,
@model.AddressRange::new(cursor, allowed.start.min(range.end)),
)
}
cursor = cursor.max(allowed.end.min(range.end))
index += 1
}
if cursor < range.end {
outside_issue(issues, @model.AddressRange::new(cursor, range.end))
}
}
let checked_entry = validate_entry(image, regions, options, issues)
{
issues,
payload_bytes: payload,
address_span: span,
segment_count: image.memory.segment_count(),
checked_entry,
}
}
///|
fn outside_issue(
issues : Array[LayoutIssue],
range : @model.AddressRange,
) -> Unit {
issues.push({
rule: OutsideAllowedRegions,
level: Error,
range: Some(range),
region: None,
message: "occupied bytes lie outside all allowed target regions",
})
}
///|
fn validate_entry(
image : @model.FirmwareImage,
regions : Array[MemoryRegion],
options : LayoutOptions,
issues : Array[LayoutIssue],
) -> Int64? raise @model.FirmwareError {
let entry = match image.entry {
None => {
if options.entry_required != Disabled {
issues.push({
rule: MissingEntry,
level: options.entry_required,
range: None,
region: None,
message: "firmware has no execution entry point",
})
}
return None
}
Some(value) => value.address()
}
let address = if options.clear_entry_thumb_bit {
entry - entry % 2L
} else {
entry
}
let range = @model.AddressRange::new(address, address + 1L)
if address % options.entry_alignment.to_int64() != 0L {
issues.push({
rule: EntryAlignment,
level: Error,
range: Some(range),
region: None,
message: "entry address must align to \{options.entry_alignment} bytes",
})
}
if options.entry_mapped != Disabled && !image.memory.contains(address) {
issues.push({
rule: EntryNotMapped,
level: options.entry_mapped,
range: Some(range),
region: None,
message: "entry address has no occupied firmware byte",
})
}
if options.entry_executable != Disabled {
let mut enclosing : MemoryRegion? = None
for region in regions {
if region.range.contains(address) {
enclosing = Some(region)
break
}
}
match enclosing {
Some(region) =>
if !region.executable {
issues.push({
rule: EntryNotExecutable,
level: options.entry_executable,
range: Some(range),
region: Some(region.name),
message: "entry address belongs to a non-executable target region",
})
}
None =>
issues.push({
rule: EntryNotExecutable,
level: options.entry_executable,
range: Some(range),
region: None,
message: "entry address lies outside all executable target regions",
})
}
}
Some(address)
}