Skip to main content

presolve_compiler/
structural_occurrence_identity.rs

1//! Opaque runtime occurrence identity codec shared by structural artifacts.
2
3const PREFIX: &str = "presolve-structural-occurrence:v1:";
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct StructuralOccurrenceIdentity {
7    pub parent_scope: String,
8    pub region: String,
9    pub template_instance: String,
10    pub local_occurrence: String,
11}
12
13/// Encode the four compiler/runtime-issued identity inputs with the frozen v1
14/// framing. Inputs must be non-empty; callers own their prior validation.
15#[must_use]
16pub fn encode_structural_occurrence_identity(
17    parent_scope: &str,
18    region: &str,
19    template_instance: &str,
20    local_occurrence: &str,
21) -> String {
22    format!(
23        "{PREFIX}{}.{}.{}.{}",
24        hex(parent_scope),
25        hex(region),
26        hex(template_instance),
27        hex(local_occurrence)
28    )
29}
30
31/// Decode a v1 opaque occurrence identity without interpreting its fields as
32/// semantic IDs, list keys, or DOM addresses.
33pub fn decode_structural_occurrence_identity(
34    value: &str,
35) -> Result<StructuralOccurrenceIdentity, String> {
36    let fields = value
37        .strip_prefix(PREFIX)
38        .ok_or_else(|| "structural occurrence identity has an invalid prefix".to_string())?
39        .split('.')
40        .collect::<Vec<_>>();
41    if fields.len() != 4 || fields.iter().any(|field| field.is_empty()) {
42        return Err("structural occurrence identity has an invalid field count".to_string());
43    }
44    let decoded = fields
45        .into_iter()
46        .map(decode_hex)
47        .collect::<Result<Vec<_>, _>>()?;
48    Ok(StructuralOccurrenceIdentity {
49        parent_scope: decoded[0].clone(),
50        region: decoded[1].clone(),
51        template_instance: decoded[2].clone(),
52        local_occurrence: decoded[3].clone(),
53    })
54}
55
56fn hex(value: &str) -> String {
57    value
58        .as_bytes()
59        .iter()
60        .map(|byte| format!("{byte:02X}"))
61        .collect()
62}
63
64fn decode_hex(value: &str) -> Result<String, String> {
65    if value.len() % 2 != 0 {
66        return Err("structural occurrence identity has an odd hex field".to_string());
67    }
68    let bytes = (0..value.len())
69        .step_by(2)
70        .map(|index| u8::from_str_radix(&value[index..index + 2], 16))
71        .collect::<Result<Vec<_>, _>>()
72        .map_err(|_| "structural occurrence identity has invalid hex".to_string())?;
73    let decoded = String::from_utf8(bytes)
74        .map_err(|_| "structural occurrence identity has invalid UTF-8".to_string())?;
75    if decoded.is_empty() {
76        return Err("structural occurrence identity has an empty decoded field".to_string());
77    }
78    Ok(decoded)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::{decode_structural_occurrence_identity, encode_structural_occurrence_identity};
84
85    #[test]
86    fn round_trips_utf8_and_uses_uppercase_byte_framing() {
87        let encoded =
88            encode_structural_occurrence_identity("parent:α", "region", "template", "key:7");
89        assert_eq!(encoded, "presolve-structural-occurrence:v1:706172656E743ACEB1.726567696F6E.74656D706C617465.6B65793A37");
90        assert_eq!(
91            decode_structural_occurrence_identity(&encoded)
92                .unwrap()
93                .local_occurrence,
94            "key:7"
95        );
96    }
97
98    #[test]
99    fn rejects_malformed_or_empty_fields() {
100        for value in [
101            "presolve-structural-occurrence:v2:61.62.63.64",
102            "presolve-structural-occurrence:v1:61.62.63",
103            "presolve-structural-occurrence:v1:61..63.64",
104            "presolve-structural-occurrence:v1:6.62.63.64",
105            "presolve-structural-occurrence:v1:GG.62.63.64",
106            "presolve-structural-occurrence:v1:.62.63.64",
107        ] {
108            assert!(
109                decode_structural_occurrence_identity(value).is_err(),
110                "{value}"
111            );
112        }
113    }
114}