///|
/// Bounded whole-page programming settings. Page boundaries are aligned to
/// address zero. erase_value must be chosen explicitly for the target device.
/// allowed_range, when present, must contain every entire touched page.
pub(all) struct FlashOptions {
page_size : Int
erase_value : Byte
max_pages : Int
max_output_bytes : Int
allowed_range : @model.AddressRange?
} derive(Eq, Debug)
///|
/// Plan with an explicit erase value and conservative allocation budgets.
/// Page size must be a power of two from 1 through 1048576 bytes.
pub fn FlashOptions::new(page_size : Int, erase_value : Byte) -> FlashOptions {
{
page_size,
erase_value,
max_pages: 65536,
max_output_bytes: 16 * 1024 * 1024,
allowed_range: None,
}
}
///|
/// One complete programming page. source_ranges identifies original occupied
/// bytes so applications can distinguish payload from erased padding, even
/// when payload bytes happen to equal the erase value.
pub struct FlashPage {
range : @model.AddressRange
data : Bytes
source_ranges : Array[@model.AddressRange]
} derive(Eq, Debug)
///|
/// Deterministic pages sorted by address. Only touched pages are included;
/// untouched address gaps are not erased or represented in this plan.
/// This is a data plan, not a flash programmer or device erase instruction.
pub struct FlashPlan {
pages : Array[FlashPage]
payload_bytes : Int
output_bytes : Int
erase_fill_bytes : Int
page_size : Int
erase_value : Byte
} derive(Eq, Debug)
///|
/// Build complete bytes only for pages containing occupied firmware addresses.
/// Missing bytes within touched pages use erase_value, including bytes outside
/// image bounds. This deliberately does not preserve existing device content:
/// callers must obtain/merge that content first when retention is required.
/// Page count, allocation limits and permitted programming bounds are checked
/// before page buffers are allocated. The input image is never modified.
pub fn plan_flash_pages(
image : @model.FirmwareImage,
options : FlashOptions,
) -> FlashPlan raise @model.FirmwareError {
if !valid_alignment(options.page_size) {
raise @model.FirmwareError(
@model.diagnostic(
InvalidOption,
"flash page size must be a power of two between 1 and 1048576",
),
)
}
if options.max_pages < 0 ||
options.max_pages > 65536 ||
options.max_output_bytes < 0 ||
options.max_output_bytes > 64 * 1024 * 1024 {
raise @model.FirmwareError(
@model.diagnostic(
InvalidOption,
"flash limits must fit 65536 pages and 64 MiB output",
),
)
}
let segments = image.memory.segments()
let page_size = options.page_size.to_int64()
let touched = touched_page_ranges(segments, page_size)
let mut page_count = 0L
for range in touched {
if options.allowed_range is Some(allowed) {
if range.start < allowed.start || range.end > allowed.end {
raise @model.FirmwareError(
@model.diagnostic(
InvalidRange,
"complete flash pages exceed the permitted programming range",
address=range.start,
end_address=range.end - 1L,
),
)
}
}
page_count += range.length() / page_size
}
if page_count > options.max_pages.to_int64() {
raise @model.FirmwareError(
@model.diagnostic(
ResourceLimit,
"flash plan needs \{page_count} pages; limit is \{options.max_pages}",
),
)
}
let output_bytes = page_count * page_size
if output_bytes > options.max_output_bytes.to_int64() {
raise @model.FirmwareError(
@model.diagnostic(
ResourceLimit,
"flash plan needs \{output_bytes} bytes; limit is \{options.max_output_bytes}",
),
)
}
let pages = []
let mut first_segment = 0
for extent in touched {
let mut start = extent.start
while start < extent.end {
let range = @model.AddressRange::new(start, start + page_size)
while first_segment < segments.length() &&
segments[first_segment].range().end <= start {
first_segment += 1
}
let data = Array::make(options.page_size, options.erase_value)
let source_ranges = []
let mut index = first_segment
while index < segments.length() && segments[index].start < range.end {
let segment = segments[index]
if segment.range().intersection(range) is Some(source) {
source_ranges.push(source)
let source_offset = (source.start - segment.start).to_int()
let page_offset = (source.start - range.start).to_int()
for offset in 0.. Array[@model.AddressRange] raise @model.FirmwareError {
let ranges : Array[@model.AddressRange] = []
for segment in segments {
let start = segment.start - segment.start % page_size
let last = segment.range().end - 1L
let end = last - last % page_size + page_size
if ranges.last() is Some(previous) {
if start <= previous.end {
ranges[ranges.length() - 1] = @model.AddressRange::new(
previous.start,
previous.end.max(end),
)
continue
}
}
ranges.push(@model.AddressRange::new(start, end))
}
ranges
}
///|
/// Summarize programmed addresses and padding without rendering payload bytes.
/// The manifest helps inspect an erase plan before any device is contacted.
pub fn FlashPlan::render_manifest(self : FlashPlan) -> String {
let out = StringBuilder()
out.write_string("Flash pages: \{self.pages.length()}\n")
out.write_string("Page size: \{self.page_size} bytes\n")
out.write_string("Payload: \{self.payload_bytes} bytes\n")
out.write_string("Output: \{self.output_bytes} bytes\n")
out.write_string("Erase padding: \{self.erase_fill_bytes} bytes\n")
out.write_string(
"Erase value: 0x" +
self.erase_value.to_int().to_string(radix=16).to_upper().pad_start(2, '0') +
"\n",
)
for page in self.pages {
let mut occupied = 0L
for source in page.source_ranges {
occupied += source.length()
}
out.write_string(page.range.render() + ": \{occupied} payload bytes\n")
}
out.to_string()
}