presolve_compiler/
structural_occurrence_identity.rs1const 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#[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
31pub 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}