///|
fn sorted_unique(values : Array[String]) -> Array[String] {
let out : Array[String] = []
let seen : @hashset.HashSet[String] = @hashset.HashSet([])
for value in values {
if !seen.contains(value) {
seen.add(value)
out.push(value)
}
}
out.sort_by(fn(a, b) { String::lexical_compare(a, b) })
out
}
///|
fn snapshot_identities(entries : Array[InventoryEntry]) -> (Array[String], Int) {
let identities : Array[String] = []
let mut invalid = 0
for entry in entries {
let value = parse(entry.purl) catch {
_ => {
invalid += 1
continue
}
}
identities.push(value.identity())
}
(sorted_unique(identities), invalid)
}
///|
/// Compare two inventories using normalized package-version identity. Artifact
/// qualifiers and subpaths are excluded, matching vulnerability-database join
/// behavior. Results are sorted so CI output is independent of input ordering.
pub fn diff_inventory(
before : Array[InventoryEntry],
after : Array[InventoryEntry],
) -> InventoryDelta {
let (old_values, invalid_before) = snapshot_identities(before)
let (new_values, invalid_after) = snapshot_identities(after)
let old_set : @hashset.HashSet[String] = @hashset.HashSet(old_values)
let new_set : @hashset.HashSet[String] = @hashset.HashSet(new_values)
let added : Array[String] = []
let removed : Array[String] = []
let unchanged : Array[String] = []
for value in old_values {
if new_set.contains(value) {
unchanged.push(value)
} else {
removed.push(value)
}
}
for value in new_values {
if !old_set.contains(value) {
added.push(value)
}
}
{ added, removed, unchanged, invalid_before, invalid_after, }
}