///|
/// Stable reasons why a Cortex-M release image is rejected by its target gate.
pub(all) enum ReleaseRule {
VectorTableMissing
StackPointerOutsideRam
StackPointerMisaligned
ResetThumbBitMissing
ResetHandlerOutsideFlash
ResetHandlerUnmapped
} derive(Eq, Debug)
///|
/// One target-specific release violation with an optional relevant address.
pub struct ReleaseIssue {
rule : ReleaseRule
address : Int64?
message : String
} derive(Eq, Debug)
///|
pub fn ReleaseIssue::category(self : ReleaseIssue) -> ReleaseRule {
self.rule
}
///|
pub fn ReleaseIssue::at_address(self : ReleaseIssue) -> Int64? {
self.address
}
///|
pub fn ReleaseIssue::description(self : ReleaseIssue) -> String {
self.message
}
///|
/// Explicit Cortex-M target contract. RAM ranges accept a stack pointer at
/// their exclusive end because startup stacks commonly begin at the top of RAM.
/// Flash boundaries must be aligned to page_size so a whole-page plan cannot
/// silently touch bytes outside the declared programming region.
pub(all) struct CortexMReleaseOptions {
flash : @model.AddressRange
ram : Array[@model.AddressRange]
vector_address : Int64
page_size : Int
erase_value : Byte
stack_alignment : Int
max_pages : Int
max_output_bytes : Int
} derive(Eq, Debug)
///|
/// Construct a conservative release contract for one Flash and one RAM range.
pub fn CortexMReleaseOptions::new(
flash : @model.AddressRange,
ram : @model.AddressRange,
vector_address? : Int64 = flash.start,
page_size? : Int = 1024,
erase_value? : Byte = 0xFF,
) -> CortexMReleaseOptions {
{
flash,
ram: [ram],
vector_address,
page_size,
erase_value,
stack_alignment: 8,
max_pages: 65536,
max_output_bytes: 16 * 1024 * 1024,
}
}
///|
/// Complete release-gate evidence. Layout and page plans remain available to
/// callers; target-specific vector findings are accumulated instead of failing
/// at the first bad field.
pub struct CortexMReleaseReport {
options : CortexMReleaseOptions
layout : LayoutReport
vectors : @analysis.CortexMVectorTable?
plan : FlashPlan?
issues : Array[ReleaseIssue]
} derive(Eq, Debug)
///|
/// A release passes only when address layout and Cortex-M vector checks pass
/// and a bounded whole-page programming plan can be produced.
pub fn CortexMReleaseReport::is_valid(self : CortexMReleaseReport) -> Bool {
self.layout.is_valid() && self.issues.is_empty() && self.plan is Some(_)
}
///|
/// Target-specific findings in deterministic validation order.
pub fn CortexMReleaseReport::release_issues(
self : CortexMReleaseReport,
) -> Array[ReleaseIssue] {
self.issues.copy()
}
///|
/// Generic payload and optional metadata-entry layout evidence.
pub fn CortexMReleaseReport::layout_report(
self : CortexMReleaseReport,
) -> LayoutReport {
self.layout
}
///|
/// Validated flash-page plan, absent when layout or vector checks reject.
pub fn CortexMReleaseReport::flash_plan(
self : CortexMReleaseReport,
) -> FlashPlan? {
self.plan
}
///|
/// Decoded vector information, absent when the first eight bytes are missing.
pub fn CortexMReleaseReport::vector_table(
self : CortexMReleaseReport,
) -> @analysis.CortexMVectorTable? {
self.vectors
}
///|
fn release_option_error(message : String) -> @model.FirmwareError {
@model.FirmwareError(@model.diagnostic(InvalidOption, message))
}
///|
fn checked_release_options(
options : CortexMReleaseOptions,
) -> Unit raise @model.FirmwareError {
if options.flash.is_empty() || options.ram.is_empty() {
raise release_option_error(
"release target requires non-empty Flash and RAM ranges",
)
}
if options.ram.length() > 32 {
raise release_option_error("release target permits at most 32 RAM ranges")
}
for range in options.ram {
if range.is_empty() {
raise release_option_error("RAM regions must be non-empty")
}
}
if !valid_alignment(options.page_size) ||
options.flash.start % options.page_size.to_int64() != 0L ||
options.flash.end % options.page_size.to_int64() != 0L {
raise release_option_error(
"Flash boundaries must align to a power-of-two page size",
)
}
if options.vector_address < options.flash.start ||
options.vector_address > options.flash.end - 8L {
raise release_option_error("vector table must fit inside the Flash range")
}
if options.stack_alignment < 1 ||
options.stack_alignment > 1024 ||
(options.stack_alignment & (options.stack_alignment - 1)) != 0 {
raise release_option_error(
"stack alignment must be a power of two up to 1024",
)
}
if options.max_pages < 0 ||
options.max_pages > 65536 ||
options.max_output_bytes < 0 ||
options.max_output_bytes > 64 * 1024 * 1024 {
raise release_option_error("release plan limits exceed supported bounds")
}
}
///|
fn vector_bytes_present(image : @model.FirmwareImage, address : Int64) -> Bool {
for offset in 0..<8 {
if !image.memory.contains(address + offset.to_int64()) {
return false
}
}
true
}
///|
fn stack_in_ram(stack : Int64, ranges : Array[@model.AddressRange]) -> Bool {
ranges.any(range => stack >= range.start && stack <= range.end)
}
///|
/// Validate a firmware image as a Cortex-M release artifact and build the exact
/// set of touched whole Flash pages. This function performs no device I/O.
pub fn validate_cortex_m_release(
image : @model.FirmwareImage,
options : CortexMReleaseOptions,
) -> CortexMReleaseReport raise @model.FirmwareError {
checked_release_options(options)
let layout = validate_layout(image, {
..LayoutOptions::new([
{ name: "flash", range: options.flash, executable: true, },
]),
entry_mapped: Error,
entry_executable: Error,
entry_alignment: 2,
clear_entry_thumb_bit: true,
})
let issues : Array[ReleaseIssue] = []
let mut vectors : @analysis.CortexMVectorTable? = None
if !vector_bytes_present(image, options.vector_address) {
issues.push({
rule: VectorTableMissing,
address: Some(options.vector_address),
message: "the first eight vector-table bytes are not fully mapped",
})
} else {
let decoded = @analysis.inspect_cortex_m_vectors(
image,
options.vector_address,
stack_alignment=options.stack_alignment,
)
vectors = Some(decoded)
let stack = decoded.stack_pointer()
if !stack_in_ram(stack, options.ram) {
issues.push({
rule: StackPointerOutsideRam,
address: Some(stack),
message: "initial stack pointer is outside declared RAM",
})
}
if !decoded.is_stack_aligned() {
issues.push({
rule: StackPointerMisaligned,
address: Some(stack),
message: "initial stack pointer does not meet target alignment",
})
}
let reset = decoded.reset_handler()
if !decoded.has_thumb_bit() {
issues.push({
rule: ResetThumbBitMissing,
address: Some(decoded.raw_reset_vector()),
message: "Cortex-M reset vector does not set the Thumb-state bit",
})
}
if !options.flash.contains(reset) {
issues.push({
rule: ResetHandlerOutsideFlash,
address: Some(reset),
message: "reset handler is outside declared Flash",
})
}
if !decoded.is_reset_mapped() {
issues.push({
rule: ResetHandlerUnmapped,
address: Some(reset),
message: "reset handler address is absent from the firmware image",
})
}
}
let plan = if layout.is_valid() && issues.is_empty() {
Some(
plan_flash_pages(image, {
..FlashOptions::new(options.page_size, options.erase_value),
max_pages: options.max_pages,
max_output_bytes: options.max_output_bytes,
allowed_range: Some(options.flash),
}),
)
} else {
None
}
{ options, layout, vectors, plan, issues, }
}
///|
/// Render release evidence without embedding firmware payload bytes.
pub fn CortexMReleaseReport::render(self : CortexMReleaseReport) -> String {
let out = StringBuilder()
out.write_string(
if self.is_valid() {
"Release gate: passed\n"
} else {
"Release gate: failed\n"
},
)
out.write_string("Flash: " + self.options.flash.render() + "\n")
out.write_string(
"Vector table: " + @model.hex_address(self.options.vector_address) + "\n",
)
match self.vectors {
Some(table) => {
out.write_string(
"Initial stack pointer: " +
@model.hex_address(table.stack_pointer()) +
"\n",
)
out.write_string(
"Reset handler: " + @model.hex_address(table.reset_handler()) + "\n",
)
}
None => ()
}
for issue in self.issues {
out.write_string("error: " + issue.message)
if issue.address is Some(address) {
out.write_string(" at " + @model.hex_address(address))
}
out.write_char('\n')
}
if !self.layout.is_valid() {
out.write_string(self.layout.render())
}
if self.plan is Some(plan) {
out.write_string(plan.render_manifest())
}
out.to_string()
}