Skip to main content

observer_core/
normalize.rs

1// SPDX-FileCopyrightText: 2026 Alexander R. Croft
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use crate::error::{ObserverError, ObserverResult};
5use crate::inventory::Inventory;
6use crate::product::ProductDefinition;
7use crate::suite::SuiteCore;
8use serde_json::{Map, Value};
9use sha2::{Digest, Sha256};
10
11pub fn normalized_inventory_sha256(inventory: &Inventory) -> ObserverResult<String> {
12    Ok(hash_hex(inventory.to_canonical_text().as_bytes()))
13}
14
15pub fn normalized_suite_sha256(suite: &SuiteCore) -> ObserverResult<String> {
16    let value = serde_json::to_value(suite).map_err(|error| ObserverError::Normalize(error.to_string()))?;
17    let value = wrap_suite_core(value);
18    let canonical = canonicalize_json(value);
19    let bytes = serde_json::to_vec(&canonical)
20        .map_err(|error| ObserverError::Normalize(error.to_string()))?;
21    Ok(hash_hex(&bytes))
22}
23
24pub fn normalized_product_sha256(product: &ProductDefinition) -> ObserverResult<String> {
25	product.validate()?;
26	let value = serde_json::to_value(product).map_err(|error| ObserverError::Normalize(error.to_string()))?;
27	let canonical = canonicalize_json(value);
28	let bytes = serde_json::to_vec(&canonical)
29		.map_err(|error| ObserverError::Normalize(error.to_string()))?;
30	Ok(hash_hex(&bytes))
31}
32
33fn hash_hex(bytes: &[u8]) -> String {
34    let digest = Sha256::digest(bytes);
35    format!("{digest:x}")
36}
37
38fn wrap_suite_core(items: Value) -> Value {
39    let mut object = Map::new();
40    object.insert("k".to_owned(), Value::String("observer_suite_core".to_owned()));
41    object.insert("v".to_owned(), Value::String("0".to_owned()));
42    match items {
43        Value::Object(mut suite) => {
44            let items = suite.remove("items").unwrap_or(Value::Array(Vec::new()));
45            object.insert("items".to_owned(), items);
46        }
47        _ => {
48            object.insert("items".to_owned(), Value::Array(Vec::new()));
49        }
50    }
51    Value::Object(object)
52}
53
54fn canonicalize_json(value: Value) -> Value {
55    match value {
56        Value::Object(object) => {
57            let mut ordered = Map::new();
58            let mut entries = object.into_iter().collect::<Vec<_>>();
59            entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
60            for (key, value) in entries {
61                ordered.insert(key, canonicalize_json(value));
62            }
63            Value::Object(ordered)
64        }
65        Value::Array(items) => Value::Array(items.into_iter().map(canonicalize_json).collect()),
66        other => other,
67    }
68}