ifc_lite_export/merged/
guid.rs1use std::collections::HashSet;
9
10use crate::rooted_type::is_rooted_type;
11
12const GLOBAL_ID_CHARS: &[u8; 64] =
14 b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
15
16pub fn deterministic_global_id(seed: &str) -> String {
25 let mut h0: u32 = 0x811c_9dc5;
26 let mut h1: u32 = 0x9e37_79b9;
27 let mut h2: u32 = 0x6c07_8965;
28 let mut h3: u32 = 0xb529_7a4d;
29 for unit in seed.encode_utf16() {
30 let c = unit as u32;
31 h0 = (h0 ^ c).wrapping_mul(0x0100_0193);
32 h1 = (h1 ^ c ^ (h1 >> 11)).wrapping_mul(0x85eb_ca6b);
33 h2 = h2.wrapping_add(c).wrapping_add(h2 >> 7).wrapping_mul(0xc2b2_ae35);
34 h3 = (h3 ^ ((c << 3) | (c >> 5)) ^ (h3 >> 13)).wrapping_mul(0x27d4_eb2f);
35 }
36 let mix = |x: u32, y: u32| -> u32 {
37 ((x ^ y).wrapping_add((x >> 7) | (y << 25))).wrapping_mul(0x85eb_ca6b)
38 };
39 let m0 = mix(h0, h2);
40 let m1 = mix(h1, h3);
41 let m2 = mix(h2, m1);
42 let m3 = mix(h3, m0);
43 let words = [m0, m1, m2, m3];
44
45 let bit = |idx: usize| -> u32 {
47 let word = words[idx / 32];
48 let shift = 31 - (idx % 32);
49 (word >> shift) & 1
50 };
51 let mut out = String::with_capacity(22);
52 out.push(GLOBAL_ID_CHARS[((bit(0) << 1) | bit(1)) as usize] as char);
53 for i in 0..21 {
54 let mut v = 0u32;
55 for b in 0..6 {
56 v = (v << 1) | bit(2 + i * 6 + b);
57 }
58 out.push(GLOBAL_ID_CHARS[v as usize] as char);
59 }
60 out
61}
62
63pub fn is_global_id(s: &str) -> bool {
65 s.len() == 22
66 && s.bytes()
67 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'$')
68}
69
70pub fn is_relationship_type(type_upper: &str) -> bool {
74 type_upper.starts_with("IFCREL")
75}
76
77pub fn is_rooted_entity_type(type_upper: &str) -> bool {
88 is_rooted_type(type_upper)
89}
90
91pub fn extract_global_id_fast(type_upper: &str, line: &[u8]) -> Option<String> {
95 if !is_rooted_entity_type(type_upper) {
96 return None;
97 }
98 let window = &line[..line.len().min(128)];
99 let open = window.iter().position(|&b| b == b'(')?;
100 let mut i = open + 1;
101 while i < window.len() && window[i].is_ascii_whitespace() {
102 i += 1;
103 }
104 if i >= window.len() || window[i] != b'\'' {
105 return None;
106 }
107 let start = i + 1;
108 let close = window[start..].iter().position(|&b| b == b'\'')? + start;
109 let inner = std::str::from_utf8(&window[start..close]).ok()?;
110 is_global_id(inner).then(|| inner.to_string())
111}
112
113pub fn entity_type_upper(line: &[u8]) -> Option<String> {
116 let eq = line.iter().position(|&b| b == b'=')?;
117 let mut i = eq + 1;
118 while i < line.len() && line[i].is_ascii_whitespace() {
119 i += 1;
120 }
121 let start = i;
122 while i < line.len() && line[i] != b'(' && !line[i].is_ascii_whitespace() {
123 i += 1;
124 }
125 if i == start {
126 return None;
127 }
128 std::str::from_utf8(&line[start..i]).ok().map(|s| s.to_ascii_uppercase())
129}
130
131pub fn leading_rooted_global_id(line: &[u8]) -> Option<String> {
138 let type_upper = entity_type_upper(line)?;
139 extract_global_id_fast(&type_upper, line)
140}
141
142pub fn read_leading_guid(text: &str) -> Option<String> {
147 let b = text.as_bytes();
148 let open = b.iter().position(|&c| c == b'(')?;
149 let q1 = b[open..].iter().position(|&c| c == b'\'')? + open;
150 let q2 = b[q1 + 1..].iter().position(|&c| c == b'\'')? + q1 + 1;
151 let inner = &text[q1 + 1..q2];
152 is_global_id(inner).then(|| inner.to_string())
153}
154
155pub fn replace_global_id(text: &str, new_guid: &str) -> String {
160 let b = text.as_bytes();
161 let Some(open) = b.iter().position(|&c| c == b'(') else {
162 return text.to_string();
163 };
164 let Some(q1rel) = b[open..].iter().position(|&c| c == b'\'') else {
165 return text.to_string();
166 };
167 let q1 = open + q1rel;
168 let Some(q2rel) = b[q1 + 1..].iter().position(|&c| c == b'\'') else {
169 return text.to_string();
170 };
171 let q2 = q1 + 1 + q2rel;
172 let mut out = String::with_capacity(text.len() + new_guid.len());
173 out.push_str(&text[..q1 + 1]);
174 out.push_str(new_guid);
175 out.push_str(&text[q2..]);
176 out
177}
178
179#[derive(Default)]
184pub struct GuidMinter {
185 pending: HashSet<String>,
186}
187
188impl GuidMinter {
189 pub fn new() -> Self {
190 Self::default()
191 }
192
193 pub fn mint(
197 &mut self,
198 original: &str,
199 model_id: &str,
200 emitted: &HashSet<String>,
201 also: &HashSet<String>,
202 ) -> String {
203 let mut candidate = deterministic_global_id(&format!("{original}#{model_id}"));
204 let mut n: u32 = 0;
205 while emitted.contains(&candidate)
206 || also.contains(&candidate)
207 || self.pending.contains(&candidate)
208 {
209 candidate = deterministic_global_id(&format!("{original}#{model_id}#{n}"));
210 n = n.wrapping_add(1);
211 }
212 self.pending.insert(candidate.clone());
213 candidate
214 }
215}
216
217#[cfg(test)]
218#[path = "guid_tests.rs"]
219mod guid_tests;