use crate::error::{DataError, DataErrorKind};
use crate::helpers;
use alloc::borrow::Cow;
use core::fmt;
use core::fmt::Write;
use core::ops::Deref;
use writeable::{LengthHint, Writeable};
use zerovec::ule::*;
#[doc(hidden)]
#[macro_export]
macro_rules! leading_tag {
() => {
"\nicu4x_key_tag"
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! trailing_tag {
() => {
"\n"
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! tagged {
($without_tags:expr) => {
concat!(
$crate::leading_tag!(),
$without_tags,
$crate::trailing_tag!()
)
};
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, ULE)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct DataKeyHash([u8; 4]);
impl DataKeyHash {
const fn compute_from_path(path: DataKeyPath) -> Self {
let hash = helpers::fxhash_32(
path.tagged.as_bytes(),
leading_tag!().len(),
trailing_tag!().len(),
);
Self(hash.to_le_bytes())
}
pub const fn to_bytes(self) -> [u8; 4] {
self.0
}
}
impl<'a> zerovec::maps::ZeroMapKV<'a> for DataKeyHash {
type Container = zerovec::ZeroVec<'a, DataKeyHash>;
type Slice = zerovec::ZeroSlice<DataKeyHash>;
type GetType = <DataKeyHash as AsULE>::ULE;
type OwnedType = DataKeyHash;
}
impl AsULE for DataKeyHash {
type ULE = Self;
#[inline]
fn to_unaligned(self) -> Self::ULE {
self
}
#[inline]
fn from_unaligned(unaligned: Self::ULE) -> Self {
unaligned
}
}
unsafe impl EqULE for DataKeyHash {}
#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
#[non_exhaustive]
pub enum FallbackPriority {
Language,
Region,
Collation,
}
impl FallbackPriority {
pub const fn const_default() -> Self {
Self::Language
}
}
impl Default for FallbackPriority {
fn default() -> Self {
Self::const_default()
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
#[non_exhaustive]
pub enum FallbackSupplement {
Collation,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct DataKeyPath {
tagged: &'static str,
}
impl DataKeyPath {
#[inline]
pub const fn get(self) -> &'static str {
unsafe {
core::str::from_utf8_unchecked(core::mem::transmute((
self.tagged.as_ptr().add(leading_tag!().len()),
self.tagged.len() - trailing_tag!().len() - leading_tag!().len(),
)))
}
}
}
impl Deref for DataKeyPath {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
self.get()
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
#[non_exhaustive]
pub struct DataKeyMetadata {
pub fallback_priority: FallbackPriority,
pub extension_key: Option<icu_locid::extensions::unicode::Key>,
pub fallback_supplement: Option<FallbackSupplement>,
}
impl DataKeyMetadata {
pub const fn const_default() -> Self {
Self {
fallback_priority: FallbackPriority::const_default(),
extension_key: None,
fallback_supplement: None,
}
}
#[doc(hidden)]
pub const fn construct_internal(
fallback_priority: FallbackPriority,
extension_key: Option<icu_locid::extensions::unicode::Key>,
fallback_supplement: Option<FallbackSupplement>,
) -> Self {
Self {
fallback_priority,
extension_key,
fallback_supplement,
}
}
}
impl Default for DataKeyMetadata {
#[inline]
fn default() -> Self {
Self::const_default()
}
}
#[derive(Copy, Clone)]
pub struct DataKey {
path: DataKeyPath,
hash: DataKeyHash,
metadata: DataKeyMetadata,
}
impl PartialEq for DataKey {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash && self.path == other.path && self.metadata == other.metadata
}
}
impl Eq for DataKey {}
impl Ord for DataKey {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.path
.cmp(&other.path)
.then_with(|| self.metadata.cmp(&other.metadata))
}
}
impl PartialOrd for DataKey {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for DataKey {
#[inline]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.hash.hash(state)
}
}
impl DataKey {
#[inline]
pub const fn path(self) -> DataKeyPath {
self.path
}
#[inline]
pub const fn hashed(self) -> DataKeyHash {
self.hash
}
#[inline]
pub const fn metadata(self) -> DataKeyMetadata {
self.metadata
}
#[inline]
pub const fn from_path_and_metadata(path: DataKeyPath, metadata: DataKeyMetadata) -> Self {
Self {
path,
hash: DataKeyHash::compute_from_path(path),
metadata,
}
}
#[doc(hidden)]
#[allow(clippy::indexing_slicing)]
pub const fn construct_internal(
path: &'static str,
metadata: DataKeyMetadata,
) -> Result<Self, (&'static str, usize)> {
if path.len() < leading_tag!().len() + trailing_tag!().len() {
return Err(("tag", 0));
}
let start = leading_tag!().len();
let end = path.len() - trailing_tag!().len();
let mut i = 0;
while i < leading_tag!().len() {
if path.as_bytes()[i] != leading_tag!().as_bytes()[i] {
return Err(("tag", 0));
}
i += 1;
}
i = 0;
while i < trailing_tag!().len() {
if path.as_bytes()[end + i] != trailing_tag!().as_bytes()[i] {
return Err(("tag", end + 1));
}
i += 1;
}
match Self::validate_path_manual_slice(path, start, end) {
Ok(()) => (),
Err(e) => return Err(e),
};
let path = DataKeyPath { tagged: path };
Ok(Self {
path,
hash: DataKeyHash::compute_from_path(path),
metadata,
})
}
const fn validate_path_manual_slice(
path: &'static str,
start: usize,
end: usize,
) -> Result<(), (&'static str, usize)> {
debug_assert!(start <= end);
debug_assert!(end <= path.len());
enum State {
Empty,
Body,
At,
Version,
}
use State::*;
let mut i = start;
let mut state = Empty;
loop {
let byte = if i < end {
#[allow(clippy::indexing_slicing)] Some(path.as_bytes()[i])
} else {
None
};
state = match (state, byte) {
(Empty | Body, Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) => Body,
(Body, Some(b'/')) => Body,
(Body, Some(b'@')) => At,
(At | Version, Some(b'0'..=b'9')) => Version,
(Version, None) => {
return Ok(());
}
(Empty, _) => return Err(("[a-zA-Z0-9_]", i)),
(Body, _) => return Err(("[a-zA-z0-9_/@]", i)),
(At, _) => return Err(("[0-9]", i)),
(Version, _) => return Err(("[0-9]", i)),
};
i += 1;
}
}
pub fn match_key(self, key: Self) -> Result<(), DataError> {
if self == key {
Ok(())
} else {
Err(DataErrorKind::MissingDataKey.with_key(key))
}
}
}
#[macro_export]
macro_rules! data_key {
($path:expr) => {{
$crate::data_key!($path, $crate::DataKeyMetadata::const_default())
}};
($path:expr, $metadata:expr) => {{
const RESOURCE_KEY_MACRO_CONST: $crate::DataKey = {
match $crate::DataKey::construct_internal($crate::tagged!($path), $metadata) {
Ok(v) => v,
#[allow(clippy::panic)] Err(_) => panic!(concat!("Invalid resource key: ", $path)),
}
};
RESOURCE_KEY_MACRO_CONST
}};
}
impl fmt::Debug for DataKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("DataKey{")?;
fmt::Display::fmt(self, f)?;
f.write_char('}')?;
Ok(())
}
}
impl Writeable for DataKey {
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
self.path().write_to(sink)
}
fn writeable_length_hint(&self) -> LengthHint {
self.path().writeable_length_hint()
}
fn write_to_string(&self) -> Cow<str> {
Cow::Borrowed(self.path().get())
}
}
writeable::impl_display_with_writeable!(DataKey);
#[test]
fn test_path_syntax() {
DataKey::construct_internal(tagged!("hello/world@1"), Default::default()).unwrap();
DataKey::construct_internal(tagged!("hello/world/foo@1"), Default::default()).unwrap();
DataKey::construct_internal(tagged!("hello/world@999"), Default::default()).unwrap();
DataKey::construct_internal(tagged!("hello_world/foo@1"), Default::default()).unwrap();
DataKey::construct_internal(tagged!("hello_458/world@1"), Default::default()).unwrap();
DataKey::construct_internal(tagged!("hello_world@1"), Default::default()).unwrap();
assert_eq!(
DataKey::construct_internal(tagged!("hello/world"), Default::default()),
Err((
"[a-zA-z0-9_/@]",
concat!(leading_tag!(), "hello/world").len()
))
);
assert_eq!(
DataKey::construct_internal(tagged!("hello/world@"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "hello/world@").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("hello/world@foo"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "hello/world@").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("hello/world@1foo"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "hello/world@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[R]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[u-ca]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[R][u-ca]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[U]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[uca]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[u-"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[u-caa]"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("foo@1[R"), Default::default()),
Err(("[0-9]", concat!(leading_tag!(), "foo@1").len()))
);
assert_eq!(
DataKey::construct_internal(tagged!("你好/世界@1"), Default::default()),
Err(("[a-zA-Z0-9_]", leading_tag!().len()))
);
assert_eq!(
DataKey::construct_internal(
concat!("hello/world@1", trailing_tag!()),
Default::default()
),
Err(("tag", 0))
);
assert_eq!(
DataKey::construct_internal(concat!(leading_tag!(), "hello/world@1"), Default::default()),
Err(("tag", concat!(leading_tag!(), "hello/world@1").len()))
);
assert_eq!(
DataKey::construct_internal("hello/world@1", Default::default()),
Err(("tag", 0))
);
}
#[test]
fn test_key_to_string() {
struct KeyTestCase {
pub key: DataKey,
pub expected: &'static str,
}
for cas in [
KeyTestCase {
key: data_key!("core/cardinal@1"),
expected: "core/cardinal@1",
},
KeyTestCase {
key: data_key!("core/maxlengthsubcatg@1"),
expected: "core/maxlengthsubcatg@1",
},
KeyTestCase {
key: data_key!("core/cardinal@65535"),
expected: "core/cardinal@65535",
},
] {
writeable::assert_writeable_eq!(&cas.key, cas.expected);
}
}
#[test]
fn test_key_hash() {
struct KeyTestCase {
pub key: DataKey,
pub hash: DataKeyHash,
pub path: &'static str,
}
for cas in [
KeyTestCase {
key: data_key!("core/cardinal@1"),
hash: DataKeyHash([172, 207, 42, 236]),
path: "core/cardinal@1",
},
KeyTestCase {
key: data_key!("core/maxlengthsubcatg@1"),
hash: DataKeyHash([193, 6, 79, 61]),
path: "core/maxlengthsubcatg@1",
},
KeyTestCase {
key: data_key!("core/cardinal@65535"),
hash: DataKeyHash([176, 131, 182, 223]),
path: "core/cardinal@65535",
},
] {
assert_eq!(cas.hash, cas.key.hashed(), "{}", cas.path);
assert_eq!(cas.path, &*cas.key.path(), "{}", cas.path);
}
}