1use crate::error::{DataError, DataErrorKind};
6
7use crate::fallback::{LocaleFallbackConfig, LocaleFallbackPriority, LocaleFallbackSupplement};
8use alloc::borrow::Cow;
9use core::fmt;
10use core::fmt::Write;
11use core::ops::Deref;
12use writeable::{LengthHint, Writeable};
13use zerovec::ule::*;
14
15#[doc(hidden)]
16#[macro_export]
17macro_rules! leading_tag {
18 () => {
19 "\nicu4x_key_tag"
20 };
21}
22
23#[doc(hidden)]
24#[macro_export]
25macro_rules! trailing_tag {
26 () => {
27 "\n"
28 };
29}
30
31#[doc(hidden)]
32#[macro_export]
33macro_rules! tagged {
34 ($without_tags:expr) => {
35 concat!(
36 $crate::leading_tag!(),
37 $without_tags,
38 $crate::trailing_tag!()
39 )
40 };
41}
42
43#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, ULE)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[repr(transparent)]
49pub struct DataKeyHash([u8; 4]);
50
51impl DataKeyHash {
52 const fn compute_from_path(path: DataKeyPath) -> Self {
53 let hash = fxhash_32(
54 path.tagged.as_bytes(),
55 leading_tag!().len(),
56 trailing_tag!().len(),
57 );
58 Self(hash.to_le_bytes())
59 }
60
61 pub const fn to_bytes(self) -> [u8; 4] {
63 self.0
64 }
65}
66
67#[allow(clippy::indexing_slicing)]
84const fn fxhash_32(bytes: &[u8], ignore_leading: usize, ignore_trailing: usize) -> u32 {
85 if ignore_leading + ignore_trailing >= bytes.len() {
99 return 0;
100 }
101
102 #[inline]
103 const fn hash_word_32(mut hash: u32, word: u32) -> u32 {
104 const ROTATE: u32 = 5;
105 const SEED32: u32 = 0x9e_37_79_b9;
106 hash = hash.rotate_left(ROTATE);
107 hash ^= word;
108 hash = hash.wrapping_mul(SEED32);
109 hash
110 }
111
112 let mut cursor = ignore_leading;
113 let end = bytes.len() - ignore_trailing;
114 let mut hash = 0;
115
116 while end - cursor >= 4 {
117 let word = u32::from_le_bytes([
118 bytes[cursor],
119 bytes[cursor + 1],
120 bytes[cursor + 2],
121 bytes[cursor + 3],
122 ]);
123 hash = hash_word_32(hash, word);
124 cursor += 4;
125 }
126
127 if end - cursor >= 2 {
128 let word = u16::from_le_bytes([bytes[cursor], bytes[cursor + 1]]);
129 hash = hash_word_32(hash, word as u32);
130 cursor += 2;
131 }
132
133 if end - cursor >= 1 {
134 hash = hash_word_32(hash, bytes[cursor] as u32);
135 }
136
137 hash
138}
139
140impl<'a> zerovec::maps::ZeroMapKV<'a> for DataKeyHash {
141 type Container = zerovec::ZeroVec<'a, DataKeyHash>;
142 type Slice = zerovec::ZeroSlice<DataKeyHash>;
143 type GetType = <DataKeyHash as AsULE>::ULE;
144 type OwnedType = DataKeyHash;
145}
146
147impl AsULE for DataKeyHash {
148 type ULE = Self;
149 #[inline]
150 fn to_unaligned(self) -> Self::ULE {
151 self
152 }
153 #[inline]
154 fn from_unaligned(unaligned: Self::ULE) -> Self {
155 unaligned
156 }
157}
158
159unsafe impl EqULE for DataKeyHash {}
161
162#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
164pub struct DataKeyPath {
165 tagged: &'static str,
168}
169
170impl DataKeyPath {
171 #[inline]
173 pub const fn get(self) -> &'static str {
174 unsafe {
175 core::str::from_utf8_unchecked(core::slice::from_raw_parts(
177 self.tagged.as_ptr().add(leading_tag!().len()),
178 self.tagged.len() - trailing_tag!().len() - leading_tag!().len(),
179 ))
180 }
181 }
182}
183
184impl Deref for DataKeyPath {
185 type Target = str;
186 #[inline]
187 fn deref(&self) -> &Self::Target {
188 self.get()
189 }
190}
191
192#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
194#[non_exhaustive]
195pub struct DataKeyMetadata {
196 pub fallback_priority: LocaleFallbackPriority,
198 pub extension_key: Option<icu_locid::extensions::unicode::Key>,
200 pub fallback_supplement: Option<LocaleFallbackSupplement>,
204 pub singleton: bool,
208}
209
210impl DataKeyMetadata {
211 pub const fn const_default() -> Self {
213 Self {
214 fallback_priority: LocaleFallbackPriority::const_default(),
215 extension_key: None,
216 fallback_supplement: None,
217 singleton: false,
218 }
219 }
220
221 #[doc(hidden)]
222 pub const fn construct_internal(
223 fallback_priority: LocaleFallbackPriority,
224 extension_key: Option<icu_locid::extensions::unicode::Key>,
225 fallback_supplement: Option<LocaleFallbackSupplement>,
226 singleton: bool,
227 ) -> Self {
228 Self {
229 fallback_priority,
230 extension_key,
231 fallback_supplement,
232 singleton,
233 }
234 }
235}
236
237impl Default for DataKeyMetadata {
238 #[inline]
239 fn default() -> Self {
240 Self::const_default()
241 }
242}
243
244#[derive(Copy, Clone)]
268pub struct DataKey {
269 path: DataKeyPath,
270 hash: DataKeyHash,
271 metadata: DataKeyMetadata,
272}
273
274impl PartialEq for DataKey {
275 #[inline]
276 fn eq(&self, other: &Self) -> bool {
277 self.hash == other.hash && self.path == other.path && self.metadata == other.metadata
278 }
279}
280
281impl Eq for DataKey {}
282
283impl Ord for DataKey {
284 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
285 self.path
286 .cmp(&other.path)
287 .then_with(|| self.metadata.cmp(&other.metadata))
288 }
289}
290
291impl PartialOrd for DataKey {
292 #[inline]
293 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
294 Some(self.cmp(other))
295 }
296}
297
298impl core::hash::Hash for DataKey {
299 #[inline]
300 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
301 self.hash.hash(state)
302 }
303}
304
305impl DataKey {
306 #[inline]
313 pub const fn path(self) -> DataKeyPath {
314 self.path
315 }
316
317 #[inline]
333 pub const fn hashed(self) -> DataKeyHash {
334 self.hash
335 }
336
337 #[inline]
339 pub const fn metadata(self) -> DataKeyMetadata {
340 self.metadata
341 }
342
343 #[inline]
345 pub const fn fallback_config(self) -> LocaleFallbackConfig {
346 let mut config = LocaleFallbackConfig::const_default();
347 config.priority = self.metadata.fallback_priority;
348 config.extension_key = self.metadata.extension_key;
349 config.fallback_supplement = self.metadata.fallback_supplement;
350 config
351 }
352
353 #[inline]
369 pub const fn from_path_and_metadata(path: DataKeyPath, metadata: DataKeyMetadata) -> Self {
370 Self {
371 path,
372 hash: DataKeyHash::compute_from_path(path),
373 metadata,
374 }
375 }
376
377 #[doc(hidden)]
378 #[allow(clippy::indexing_slicing)]
381 pub const fn construct_internal(
382 path: &'static str,
383 metadata: DataKeyMetadata,
384 ) -> Result<Self, (&'static str, usize)> {
385 if path.len() < leading_tag!().len() + trailing_tag!().len() {
386 return Err(("tag", 0));
387 }
388 let start = leading_tag!().len();
390 let end = path.len() - trailing_tag!().len();
391
392 let mut i = 0;
394 while i < leading_tag!().len() {
395 if path.as_bytes()[i] != leading_tag!().as_bytes()[i] {
396 return Err(("tag", 0));
397 }
398 i += 1;
399 }
400 i = 0;
401 while i < trailing_tag!().len() {
402 if path.as_bytes()[end + i] != trailing_tag!().as_bytes()[i] {
403 return Err(("tag", end + 1));
404 }
405 i += 1;
406 }
407
408 match Self::validate_path_manual_slice(path, start, end) {
409 Ok(()) => (),
410 Err(e) => return Err(e),
411 };
412
413 let path = DataKeyPath { tagged: path };
414
415 Ok(Self {
416 path,
417 hash: DataKeyHash::compute_from_path(path),
418 metadata,
419 })
420 }
421
422 const fn validate_path_manual_slice(
423 path: &'static str,
424 start: usize,
425 end: usize,
426 ) -> Result<(), (&'static str, usize)> {
427 debug_assert!(start <= end);
428 debug_assert!(end <= path.len());
429 enum State {
431 Empty,
432 Body,
433 At,
434 Version,
435 }
436 use State::*;
437 let mut i = start;
438 let mut state = Empty;
439 loop {
440 let byte = if i < end {
441 #[allow(clippy::indexing_slicing)] Some(path.as_bytes()[i])
443 } else {
444 None
445 };
446 state = match (state, byte) {
447 (Empty | Body, Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) => Body,
448 (Body, Some(b'/')) => Body,
449 (Body, Some(b'@')) => At,
450 (At | Version, Some(b'0'..=b'9')) => Version,
451 (Version, None) => {
453 return Ok(());
454 }
455
456 (Empty, _) => return Err(("[a-zA-Z0-9_]", i)),
457 (Body, _) => return Err(("[a-zA-z0-9_/@]", i)),
458 (At, _) => return Err(("[0-9]", i)),
459 (Version, _) => return Err(("[0-9]", i)),
460 };
461 i += 1;
462 }
463 }
464
465 pub fn match_key(self, key: Self) -> Result<(), DataError> {
498 if self == key {
499 Ok(())
500 } else {
501 Err(DataErrorKind::MissingDataKey.with_key(key))
502 }
503 }
504}
505
506#[macro_export]
508macro_rules! data_key {
509 ($path:expr) => {{
510 $crate::data_key!($path, $crate::DataKeyMetadata::const_default())
511 }};
512 ($path:expr, $metadata:expr) => {{
513 const RESOURCE_KEY_MACRO_CONST: $crate::DataKey = {
515 match $crate::DataKey::construct_internal($crate::tagged!($path), $metadata) {
516 Ok(v) => v,
517 #[allow(clippy::panic)] Err(_) => panic!(concat!("Invalid resource key: ", $path)),
519 }
527 };
528 RESOURCE_KEY_MACRO_CONST
529 }};
530}
531
532impl fmt::Debug for DataKey {
533 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
534 f.write_str("DataKey{")?;
535 fmt::Display::fmt(self, f)?;
536 f.write_char('}')?;
537 Ok(())
538 }
539}
540
541impl Writeable for DataKey {
542 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
543 self.path().write_to(sink)
544 }
545
546 fn writeable_length_hint(&self) -> LengthHint {
547 self.path().writeable_length_hint()
548 }
549
550 fn write_to_string(&self) -> Cow<str> {
551 Cow::Borrowed(self.path().get())
552 }
553}
554
555writeable::impl_display_with_writeable!(DataKey);
556
557#[test]
558fn test_path_syntax() {
559 DataKey::construct_internal(tagged!("hello/world@1"), Default::default()).unwrap();
561 DataKey::construct_internal(tagged!("hello/world/foo@1"), Default::default()).unwrap();
562 DataKey::construct_internal(tagged!("hello/world@999"), Default::default()).unwrap();
563 DataKey::construct_internal(tagged!("hello_world/foo@1"), Default::default()).unwrap();
564 DataKey::construct_internal(tagged!("hello_458/world@1"), Default::default()).unwrap();
565 DataKey::construct_internal(tagged!("hello_world@1"), Default::default()).unwrap();
566
567 assert_eq!(
569 DataKey::construct_internal(tagged!("hello/world"), Default::default()),
570 Err((
571 "[a-zA-z0-9_/@]",
572 concat!(leading_tag!(), "hello/world").len()
573 ))
574 );
575
576 assert_eq!(
577 DataKey::construct_internal(tagged!("hello/world@"), Default::default()),
578 Err(("[0-9]", concat!(leading_tag!(), "hello/world@").len()))
579 );
580 assert_eq!(
581 DataKey::construct_internal(tagged!("hello/world@foo"), Default::default()),
582 Err(("[0-9]", concat!(leading_tag!(), "hello/world@").len()))
583 );
584 assert_eq!(
585 DataKey::construct_internal(tagged!("hello/world@1foo"), Default::default()),
586 Err(("[0-9]", concat!(leading_tag!(), "hello/world@1").len()))
587 );
588
589 assert_eq!(
591 DataKey::construct_internal(tagged!("foo@1[R]"), Default::default()),
592 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
593 );
594 assert_eq!(
595 DataKey::construct_internal(tagged!("foo@1[u-ca]"), Default::default()),
596 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
597 );
598 assert_eq!(
599 DataKey::construct_internal(tagged!("foo@1[R][u-ca]"), Default::default()),
600 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
601 );
602
603 assert_eq!(
605 DataKey::construct_internal(tagged!("foo@1[U]"), Default::default()),
606 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
607 );
608 assert_eq!(
609 DataKey::construct_internal(tagged!("foo@1[uca]"), Default::default()),
610 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
611 );
612 assert_eq!(
613 DataKey::construct_internal(tagged!("foo@1[u-"), Default::default()),
614 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
615 );
616 assert_eq!(
617 DataKey::construct_internal(tagged!("foo@1[u-caa]"), Default::default()),
618 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
619 );
620 assert_eq!(
621 DataKey::construct_internal(tagged!("foo@1[R"), Default::default()),
622 Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
623 );
624
625 assert_eq!(
627 DataKey::construct_internal(tagged!("你好/世界@1"), Default::default()),
628 Err(("[a-zA-Z0-9_]", leading_tag!().len()))
629 );
630
631 assert_eq!(
633 DataKey::construct_internal(
634 concat!("hello/world@1", trailing_tag!()),
635 Default::default()
636 ),
637 Err(("tag", 0))
638 );
639 assert_eq!(
640 DataKey::construct_internal(concat!(leading_tag!(), "hello/world@1"), Default::default()),
641 Err(("tag", concat!(leading_tag!(), "hello/world@1").len()))
642 );
643 assert_eq!(
644 DataKey::construct_internal("hello/world@1", Default::default()),
645 Err(("tag", 0))
646 );
647}
648
649#[test]
650fn test_key_to_string() {
651 struct KeyTestCase {
652 pub key: DataKey,
653 pub expected: &'static str,
654 }
655
656 for cas in [
657 KeyTestCase {
658 key: data_key!("core/cardinal@1"),
659 expected: "core/cardinal@1",
660 },
661 KeyTestCase {
662 key: data_key!("core/maxlengthsubcatg@1"),
663 expected: "core/maxlengthsubcatg@1",
664 },
665 KeyTestCase {
666 key: data_key!("core/cardinal@65535"),
667 expected: "core/cardinal@65535",
668 },
669 ] {
670 writeable::assert_writeable_eq!(&cas.key, cas.expected);
671 assert_eq!(cas.expected, &*cas.key.path());
672 }
673}
674
675#[test]
676fn test_hash_word_32() {
677 assert_eq!(0, fxhash_32(b"", 0, 0));
678 assert_eq!(0, fxhash_32(b"a", 1, 0));
679 assert_eq!(0, fxhash_32(b"a", 0, 1));
680 assert_eq!(0, fxhash_32(b"a", 0, 10));
681 assert_eq!(0, fxhash_32(b"a", 10, 0));
682 assert_eq!(0, fxhash_32(b"a", 1, 1));
683 assert_eq!(0xF3051F19, fxhash_32(b"a", 0, 0));
684 assert_eq!(0x2F9DF119, fxhash_32(b"ab", 0, 0));
685 assert_eq!(0xCB1D9396, fxhash_32(b"abc", 0, 0));
686 assert_eq!(0x8628F119, fxhash_32(b"abcd", 0, 0));
687 assert_eq!(0xBEBDB56D, fxhash_32(b"abcde", 0, 0));
688 assert_eq!(0x1CE8476D, fxhash_32(b"abcdef", 0, 0));
689 assert_eq!(0xC0F176A4, fxhash_32(b"abcdefg", 0, 0));
690 assert_eq!(0x09AB476D, fxhash_32(b"abcdefgh", 0, 0));
691 assert_eq!(0xB72F5D88, fxhash_32(b"abcdefghi", 0, 0));
692}
693
694#[test]
695fn test_key_hash() {
696 struct KeyTestCase {
697 pub key: DataKey,
698 pub hash: DataKeyHash,
699 }
700
701 for cas in [
702 KeyTestCase {
703 key: data_key!("core/cardinal@1"),
704 hash: DataKeyHash([172, 207, 42, 236]),
705 },
706 KeyTestCase {
707 key: data_key!("core/maxlengthsubcatg@1"),
708 hash: DataKeyHash([193, 6, 79, 61]),
709 },
710 KeyTestCase {
711 key: data_key!("core/cardinal@65535"),
712 hash: DataKeyHash([176, 131, 182, 223]),
713 },
714 ] {
715 assert_eq!(cas.hash, cas.key.hashed(), "{}", cas.key);
716 }
717}