1use std::borrow::Cow;
7use std::collections::BTreeMap;
8
9#[derive(Clone, Debug)]
11pub struct ElementId {
12 declaration: &'static Declaration,
13 text: Cow<'static, str>,
14}
15impl PartialEq for ElementId {
16 fn eq(&self, other: &Self) -> bool {
17 self.text == other.text
18 }
19}
20impl Eq for ElementId {}
21
22impl ElementId {
23 #[doc(hidden)]
24 pub fn from_declaration(declaration: &'static Declaration) -> Self {
25 Self {
26 declaration,
27 text: Cow::Borrowed(declaration.family),
28 }
29 }
30 #[doc(hidden)]
31 pub fn with_key(declaration: &'static Declaration, key: impl IntoElementKey) -> Self {
32 let mut text = String::from(declaration.family);
33 text.push(':');
34 for byte in key.into_element_key().bytes() {
35 if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) {
36 text.push(char::from(byte));
37 } else {
38 const HEX: &[u8] = b"0123456789ABCDEF";
39 text.push('%');
40 text.push(char::from(HEX[(byte >> 4) as usize]));
41 text.push(char::from(HEX[(byte & 15) as usize]));
42 }
43 }
44 Self {
45 declaration,
46 text: Cow::Owned(text),
47 }
48 }
49 pub fn as_str(&self) -> &str {
50 &self.text
51 }
52 pub fn family(&self) -> &'static str {
53 self.declaration.family
54 }
55 pub fn is_keyed(&self) -> bool {
56 self.declaration.keyed
57 }
58}
59
60pub trait IntoElementKey {
64 fn into_element_key(self) -> String;
65}
66impl IntoElementKey for String {
67 fn into_element_key(self) -> String {
68 self
69 }
70}
71impl IntoElementKey for &str {
72 fn into_element_key(self) -> String {
73 self.to_owned()
74 }
75}
76impl IntoElementKey for &String {
77 fn into_element_key(self) -> String {
78 self.clone()
79 }
80}
81macro_rules! integer_keys {
82 ($($ty:ty),*) => { $(impl IntoElementKey for $ty {
83 fn into_element_key(self) -> String { self.to_string() }
84 })* };
85}
86integer_keys!(
87 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
88);
89
90#[doc(hidden)]
91#[derive(Debug, PartialEq, Eq)]
92pub struct Declaration {
93 pub family: &'static str,
94 pub keyed: bool,
95 pub package: &'static str,
96 pub version: &'static str,
97 pub file: &'static str,
98 pub line: u32,
99}
100#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct RegisteredId {
103 pub family: String,
104 pub keyed: bool,
105 pub package: String,
106 pub version: String,
107 pub locations: Vec<String>,
108}
109#[cfg(not(target_arch = "wasm32"))]
110#[linkme::distributed_slice]
111#[doc(hidden)]
112pub static DECLARATIONS: [Declaration];
113
114pub fn registry() -> Result<&'static [RegisteredId], String> {
117 #[cfg(not(target_arch = "wasm32"))]
118 {
119 static CATALOG: std::sync::OnceLock<Result<Vec<RegisteredId>, String>> =
120 std::sync::OnceLock::new();
121 CATALOG
122 .get_or_init(|| {
123 normalize(
124 DECLARATIONS
125 .iter()
126 .map(|d| RegisteredId {
127 family: d.family.into(),
128 keyed: d.keyed,
129 package: d.package.into(),
130 version: d.version.into(),
131 locations: if d.file.is_empty() {
132 vec![]
133 } else {
134 vec![format!("{}:{}", d.file, d.line)]
135 },
136 })
137 .collect(),
138 )
139 })
140 .as_ref()
141 .map(Vec::as_slice)
142 .map_err(Clone::clone)
143 }
144 #[cfg(target_arch = "wasm32")]
145 WEB_REGISTRY
146 .get()
147 .ok_or("missing day_ids custom section handoff")?
148 .as_ref()
149 .map(Vec::as_slice)
150 .map_err(Clone::clone)
151}
152pub fn family_of(id: &str) -> &str {
154 let mut chars = id.char_indices().peekable();
155 while let Some((i, c)) = chars.next() {
156 if c == ':' {
157 if chars.peek().is_some_and(|(_, c)| *c == ':') {
158 chars.next();
159 } else {
160 return &id[..i];
161 }
162 }
163 }
164 id
165}
166
167fn normalize(records: Vec<RegisteredId>) -> Result<Vec<RegisteredId>, String> {
168 let mut catalog: BTreeMap<String, RegisteredId> = BTreeMap::new();
169 for record in records {
170 if let Some(prior) = catalog.get_mut(&record.family) {
171 if (prior.keyed, &prior.package, &prior.version)
172 != (record.keyed, &record.package, &record.version)
173 {
174 let owner = |r: &RegisteredId| {
175 let shape = if r.keyed { "keyed" } else { "static" };
176 match r.locations.first() {
177 Some(at) => format!("{shape} in {} {} at {at}", r.package, r.version),
178 None => format!("{shape} in {} {}", r.package, r.version),
179 }
180 };
181 return Err(format!(
182 "conflicting element ID declaration: {} ({}; {})",
183 record.family,
184 owner(prior),
185 owner(&record)
186 ));
187 }
188 prior.locations.extend(record.locations);
189 prior.locations.sort();
190 prior.locations.dedup();
191 } else {
192 catalog.insert(record.family.clone(), record);
193 }
194 }
195 Ok(catalog.into_values().collect())
196}
197
198#[doc(hidden)]
199pub const fn validate_name(name: &str) {
200 let bytes = name.as_bytes();
201 assert!(!bytes.is_empty(), "element ID must not be empty");
202 let mut start = 0;
203 let mut i = 0;
204 while i <= bytes.len() {
205 if i == bytes.len() || bytes[i] == b':' {
206 assert!(i > start, "empty element ID namespace segment");
207 assert!(
208 bytes[start] >= b'a' && bytes[start] <= b'z',
209 "element ID must start with a lowercase ASCII letter"
210 );
211 assert!(
212 bytes[i - 1] != b'_',
213 "element ID must not end with underscore"
214 );
215 let keywords = [
218 "as",
219 "async",
220 "await",
221 "break",
222 "const",
223 "continue",
224 "crate",
225 "dyn",
226 "else",
227 "enum",
228 "extern",
229 "false",
230 "fn",
231 "for",
232 "if",
233 "impl",
234 "in",
235 "let",
236 "loop",
237 "match",
238 "mod",
239 "move",
240 "mut",
241 "pub",
242 "ref",
243 "return",
244 "self",
245 "static",
246 "struct",
247 "super",
248 "trait",
249 "true",
250 "type",
251 "unsafe",
252 "use",
253 "where",
254 "while",
255 "abstract",
256 "become",
257 "box",
258 "do",
259 "final",
260 "gen",
261 "macro",
262 "override",
263 "priv",
264 "try",
265 "typeof",
266 "unsized",
267 "virtual",
268 "yield",
269 "union",
270 "macro_rules",
271 ];
272 let mut k = 0;
273 while k < keywords.len() {
274 let keyword = keywords[k].as_bytes();
275 let mut equal = keyword.len() == i - start;
276 let mut j = 0;
277 while equal && j < keyword.len() {
278 equal = bytes[start + j] == keyword[j];
279 j += 1;
280 }
281 assert!(!equal, "Rust keyword is not an element ID");
282 k += 1;
283 }
284 if i == bytes.len() {
285 break;
286 }
287 assert!(
288 i + 1 < bytes.len() && bytes[i + 1] == b':',
289 "invalid namespace separator"
290 );
291 i += 2;
292 start = i;
293 } else {
294 let b = bytes[i];
295 assert!(
296 (b >= b'a' && b <= b'z') || (b >= b'0' && b <= b'9') || b == b'_',
297 "element IDs use lowercase ASCII snake_case, not raw identifiers"
298 );
299 assert!(
300 b != b'_' || i == start || bytes[i - 1] != b'_',
301 "double underscore in element ID"
302 );
303 i += 1;
304 }
305 }
306}
307
308#[macro_export]
335macro_rules! id {
336 ($first:ident $(:: $rest:ident)* $(,)?) => {{
337 $crate::__declare_element_id!(false, "s", $first $(:: $rest)*);
338 $crate::ElementId::from_declaration(&__DAY_ID)
339 }};
340 ($first:ident $(:: $rest:ident)*, $key:expr $(,)?) => {{
341 $crate::__declare_element_id!(true, "k", $first $(:: $rest)*);
342 $crate::ElementId::with_key(&__DAY_ID, $key)
343 }};
344}
345#[doc(hidden)]
346#[macro_export]
347macro_rules! __declare_element_id {
348 ($keyed:expr, $flag:literal, $first:ident $(:: $rest:ident)*) => {
349 const __NAME: &str = concat!(stringify!($first), $("::", stringify!($rest)),*);
350 const _: () = $crate::element_id::validate_name(__NAME);
351 #[cfg(not(target_arch = "wasm32"))]
352 #[$crate::linkme::distributed_slice($crate::element_id::DECLARATIONS)]
353 #[linkme(crate = $crate::linkme)]
354 static __DAY_ID: $crate::element_id::Declaration = $crate::element_id::Declaration {
355 family: __NAME, keyed: $keyed, package: env!("CARGO_PKG_NAME"), version: env!("CARGO_PKG_VERSION"),
356 file: if cfg!(debug_assertions) { file!() } else { "" },
357 line: if cfg!(debug_assertions) { line!() } else { 0 },
358 };
359 #[cfg(target_arch = "wasm32")]
360 static __DAY_ID: $crate::element_id::Declaration = $crate::element_id::Declaration {
361 family: __NAME, keyed: $keyed, package: env!("CARGO_PKG_NAME"), version: env!("CARGO_PKG_VERSION"), file: "", line: 0,
362 };
363 #[cfg(target_arch = "wasm32")]
364 const __PAYLOAD: &str = concat!("D1\0", $flag, "\0", stringify!($first), $("::", stringify!($rest),)* "\0", env!("CARGO_PKG_NAME"), "\0", env!("CARGO_PKG_VERSION"));
365 #[cfg(target_arch = "wasm32")]
366 #[$crate::__retain_element_id]
367 #[used]
368 #[unsafe(link_section = "day_ids")]
369 static __DAY_ID_BYTES: [u8; __PAYLOAD.len()+4] = $crate::element_id::record_bytes(__PAYLOAD);
370 };
371}
372
373#[doc(hidden)]
374pub const fn record_bytes<const N: usize>(payload: &str) -> [u8; N] {
375 let mut out = [0; N];
376 let length = (payload.len() as u32).to_le_bytes();
377 let mut i = 0;
378 while i < 4 {
379 out[i] = length[i];
380 i += 1;
381 }
382 i = 0;
383 while i < payload.len() {
384 out[i + 4] = payload.as_bytes()[i];
385 i += 1;
386 }
387 out
388}
389#[cfg(target_arch = "wasm32")]
390#[crate::__retain_element_id]
391#[used]
392#[unsafe(link_section = "day_ids")]
393static SENTINEL: [u8; 6] = record_bytes("D1");
394#[cfg(target_arch = "wasm32")]
395static WEB_REGISTRY: std::sync::OnceLock<Result<Vec<RegisteredId>, String>> =
396 std::sync::OnceLock::new();
397#[cfg(target_arch = "wasm32")]
400pub fn install_web_registry(bytes: &[u8]) -> Result<(), String> {
401 WEB_REGISTRY
402 .set(decode_records(bytes))
403 .map_err(|_| "element registry already installed".to_owned())
404}
405#[cfg(any(test, target_arch = "wasm32"))]
406fn decode_records(mut bytes: &[u8]) -> Result<Vec<RegisteredId>, String> {
407 let mut records = Vec::new();
408 let mut sentinel = false;
409 while !bytes.is_empty() {
410 let length = bytes.get(..4).ok_or("truncated day_ids length")?;
411 let length = u32::from_le_bytes(length.try_into().unwrap()) as usize;
412 bytes = &bytes[4..];
413 let payload = bytes.get(..length).ok_or("truncated day_ids record")?;
414 bytes = &bytes[length..];
415 let text = std::str::from_utf8(payload).map_err(|_| "non UTF-8 day_ids record")?;
416 if text == "D1" {
417 sentinel = true;
418 continue;
419 }
420 let fields: Vec<_> = text.split('\0').collect();
421 if fields.len() != 5
422 || fields[0] != "D1"
423 || !matches!(fields[1], "s" | "k")
424 || fields[2].is_empty()
425 {
426 return Err("invalid day_ids schema/record".into());
427 }
428 records.push(RegisteredId {
429 family: fields[2].into(),
430 keyed: fields[1] == "k",
431 package: fields[3].into(),
432 version: fields[4].into(),
433 locations: vec![],
434 });
435 }
436 if !sentinel {
437 return Err("missing day_ids registry sentinel".into());
438 }
439 normalize(records)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 #[test]
446 fn exact_names_and_injective_keys() {
447 assert_eq!(crate::id!(identity), crate::id!(identity));
448 assert_eq!(crate::id!(account::save).as_str(), "account::save");
449 assert_eq!(
450 crate::id!(query_row, ":% café").as_str(),
451 "query_row:%3A%25%20caf%C3%A9"
452 );
453 assert_eq!(
454 crate::id!(query_row, 42).as_str(),
455 crate::id!(query_row, "42").as_str()
456 );
457 let calls = std::cell::Cell::new(0);
458 let _ = crate::id!(once, {
459 calls.set(calls.get() + 1);
460 3
461 });
462 assert_eq!(calls.get(), 1);
463 }
464 #[allow(dead_code)]
465 fn unused<T>(_: Option<T>) {
466 let _ = crate::id!(never_mounted);
467 }
468 #[test]
469 fn registry_includes_unused_generic() {
470 assert!(
471 registry()
472 .unwrap()
473 .iter()
474 .any(|d| d.family == "never_mounted")
475 );
476 }
477 #[test]
478 fn framed_metadata_and_fail_closed() {
479 let mut data = record_bytes::<6>("D1").to_vec();
480 let payload = concat!("D1\0k\0row\0app\0", "1");
481 data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
482 data.extend_from_slice(payload.as_bytes());
483 assert_eq!(decode_records(&data).unwrap()[0].family, "row");
484 assert!(decode_records(&data[6..]).is_err());
485 assert!(decode_records(&data[..data.len() - 1]).is_err());
486 }
487 #[test]
488 fn normalization_rejects_owner_and_shape_conflicts() {
489 let record = RegisteredId {
490 family: "save".into(),
491 keyed: false,
492 package: "fixture".into(),
493 version: "1".into(),
494 locations: vec!["fixture.rs:1".into()],
495 };
496 let mut duplicate = record.clone();
497 duplicate.locations = vec!["fixture.rs:2".into()];
498 let normalized = normalize(vec![record.clone(), duplicate.clone()]).unwrap();
499 assert_eq!(normalized.len(), 1);
500 assert_eq!(normalized[0].locations.len(), 2);
501 duplicate.keyed = true;
502 assert!(normalize(vec![record.clone(), duplicate.clone()]).is_err());
503 duplicate.keyed = false;
504 duplicate.package = "other_fixture".into();
505 assert_eq!(
507 normalize(vec![record, duplicate]).unwrap_err(),
508 "conflicting element ID declaration: save (static in fixture 1 at fixture.rs:1; \
509 static in other_fixture 1 at fixture.rs:2)"
510 );
511 }
512}