///|
/// Outer custom encoding accounting only; counts/deltas are not file ranges.
pub struct MetadataEncodingTotals {
custom_count : Int
custom_encoded_bytes : Int
section_header_bytes : Int
name_encoding_bytes : Int
content_bytes : Int
} derive(Eq, Debug)
///|
/// Ordinals identify physical instances within each input, never matched pairs.
pub struct MetadataComparisonGroup {
/// category, named, or unavailable_names. The latter is accounting only.
kind : String
name : String?
baseline_ordinals : Array[Int]
current_ordinals : Array[Int]
baseline : MetadataEncodingTotals
current : MetadataEncodingTotals
delta : MetadataEncodingTotals
}
///|
pub struct MetadataComparison {
baseline : MetadataReport
current : MetadataReport
delta : MetadataEncodingTotals
baseline_non_custom_bytes : Int
current_non_custom_bytes : Int
delta_non_custom_bytes : Int
delta_file_bytes : Int
categories : Array[MetadataComparisonGroup]
name_groups : Array[MetadataComparisonGroup]
comparison_scope : String
}
///|
priv enum MetadataDiffKey {
Named(String)
Unavailable
} derive(Eq, Hash)
///|
fn metadata_diff_totals(rows : Array[MetadataCustom]) -> MetadataEncodingTotals {
let mut encoded = 0
let mut headers = 0
let mut names = 0
let mut contents = 0
for row in rows {
encoded += row.encoding.size
headers += row.section_header_bytes
names += row.name_encoding.size
contents += row.content.size
}
{
custom_count: rows.length(),
custom_encoded_bytes: encoded,
section_header_bytes: headers,
name_encoding_bytes: names,
content_bytes: contents,
}
}
///|
fn metadata_diff_delta(
before : MetadataEncodingTotals,
after : MetadataEncodingTotals,
) -> MetadataEncodingTotals {
{
custom_count: after.custom_count - before.custom_count,
custom_encoded_bytes: after.custom_encoded_bytes -
before.custom_encoded_bytes,
section_header_bytes: after.section_header_bytes -
before.section_header_bytes,
name_encoding_bytes: after.name_encoding_bytes - before.name_encoding_bytes,
content_bytes: after.content_bytes - before.content_bytes,
}
}
///|
fn metadata_diff_group(
kind : String,
name : String?,
before : Array[MetadataCustom],
after : Array[MetadataCustom],
) -> MetadataComparisonGroup {
let baseline_ordinals = before.map(fn(row) { row.ordinal })
let current_ordinals = after.map(fn(row) { row.ordinal })
baseline_ordinals.sort()
current_ordinals.sort()
let baseline = metadata_diff_totals(before)
let current = metadata_diff_totals(after)
{
kind,
name,
baseline_ordinals,
current_ordinals,
baseline,
current,
delta: metadata_diff_delta(baseline, current),
}
}
///|
/// Each side is inspected exactly once. Optional name-detail partial results
/// remain successful; neither contents nor internal name records are compared.
pub fn compare_metadata(
baseline : Bytes,
current : Bytes,
) -> Result[MetadataComparison, ComparisonError] {
let before = match inspect_metadata(baseline) {
Ok(report) => report
Err(error) => return Err(Baseline(error))
}
let after = match inspect_metadata(current) {
Ok(report) => report
Err(error) => return Err(Current(error))
}
let categories = ["name", "producers", "debug", "other", "unavailable"].map(fn(
category,
) {
metadata_diff_group(
"category",
Some(category),
before.customs.filter(fn(row) { row.category == category }),
after.customs.filter(fn(row) { row.category == category }),
)
})
let buckets : Map[
MetadataDiffKey,
(Array[MetadataCustom], Array[MetadataCustom]),
] = Map([])
for (rows, is_current) in [(before.customs, false), (after.customs, true)] {
for row in rows {
let key = match row.name {
Some(name) => Named(name)
None => Unavailable
}
let pair = buckets.get_or_default(key, ([], []))
if is_current {
pair.1.push(row)
} else {
pair.0.push(row)
}
buckets.set(key, pair)
}
}
let name_groups = []
for key, pair in buckets {
let (kind, name) = match key {
Named(name) => ("named", Some(name))
Unavailable => ("unavailable_names", None)
}
name_groups.push(metadata_diff_group(kind, name, pair.0, pair.1))
}
name_groups.sort_by(fn(a, b) {
let size_order = b.delta.custom_encoded_bytes
.abs()
.compare(a.delta.custom_encoded_bytes.abs())
if size_order != 0 {
return size_order
}
let count_order = b.delta.custom_count
.abs()
.compare(a.delta.custom_count.abs())
if count_order != 0 {
return count_order
}
match (a.name, b.name) {
(Some(x), Some(y)) => x.lexical_compare(y)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => 0
}
})
let before_totals = metadata_diff_totals(before.customs)
let after_totals = metadata_diff_totals(after.customs)
let baseline_non_custom_bytes = before.file_size -
before_totals.custom_encoded_bytes
let current_non_custom_bytes = after.file_size -
after_totals.custom_encoded_bytes
Ok({
baseline: before,
current: after,
delta: metadata_diff_delta(before_totals, after_totals),
baseline_non_custom_bytes,
current_non_custom_bytes,
delta_non_custom_bytes: current_non_custom_bytes - baseline_non_custom_bytes,
delta_file_bytes: after.file_size - before.file_size,
categories,
name_groups,
comparison_scope: "Outer custom encodings only. Category and full-name groups are alternative views of the same instances. Names and zero byte deltas do not prove equal contents. Ordinals are input-local, not cross-build identities. Unavailable names share an accounting bucket only. Optional name details and opaque contents are retained, not compared. Debug classification uses a name prefix, not DWARF validation. Non-custom bytes include the Wasm header and all standard sections. No semantic equivalence, safe removal, recompiled or gzip savings are inferred.",
})
}