1 // This file is part of ICU4X. For terms of use, please see the file 2 // called LICENSE at the top level of the ICU4X source tree 3 // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). 4 5 macro_rules! impl_tinystr_subtag { 6 ( 7 $(#[$doc:meta])* 8 $name:ident, 9 $($path:ident)::+, 10 $macro_name:ident, 11 $internal_macro_name:ident, 12 $len_start:literal..=$len_end:literal, 13 $tinystr_ident:ident, 14 $validate:expr, 15 $normalize:expr, 16 $is_normalized:expr, 17 $error:ident, 18 [$good_example:literal $(,$more_good_examples:literal)*], 19 [$bad_example:literal $(, $more_bad_examples:literal)*], 20 ) => { 21 #[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Copy)] 22 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 23 #[repr(transparent)] 24 $(#[$doc])* 25 pub struct $name(tinystr::TinyAsciiStr<$len_end>); 26 27 impl $name { 28 /// A constructor which takes a str slice, parses it and 29 #[doc = concat!("produces a well-formed [`", stringify!($name), "`].")] 30 /// 31 /// # Examples 32 /// 33 /// ``` 34 #[doc = concat!("use icu_locale_core::", stringify!($($path::)+), stringify!($name), ";")] 35 /// 36 #[doc = concat!("assert!(", stringify!($name), "::try_from_str(", stringify!($good_example), ").is_ok());")] 37 #[doc = concat!("assert!(", stringify!($name), "::try_from_str(", stringify!($bad_example), ").is_err());")] 38 /// ``` 39 #[inline] 40 pub const fn try_from_str(s: &str) -> Result<Self, crate::parser::errors::ParseError> { 41 Self::try_from_utf8(s.as_bytes()) 42 } 43 44 /// See [`Self::try_from_str`] 45 pub const fn try_from_utf8( 46 code_units: &[u8], 47 ) -> Result<Self, crate::parser::errors::ParseError> { 48 #[allow(clippy::double_comparisons)] // if code_units.len() === 0 49 if code_units.len() < $len_start || code_units.len() > $len_end { 50 return Err(crate::parser::errors::ParseError::$error); 51 } 52 53 match tinystr::TinyAsciiStr::try_from_utf8(code_units) { 54 Ok($tinystr_ident) if $validate => Ok(Self($normalize)), 55 _ => Err(crate::parser::errors::ParseError::$error), 56 } 57 } 58 59 #[doc = concat!("Safely creates a [`", stringify!($name), "`] from its raw format")] 60 /// as returned by [`Self::into_raw`]. Unlike [`Self::try_from_utf8`], 61 /// this constructor only takes normalized values. 62 pub const fn try_from_raw( 63 raw: [u8; $len_end], 64 ) -> Result<Self, crate::parser::errors::ParseError> { 65 if let Ok($tinystr_ident) = tinystr::TinyAsciiStr::<$len_end>::try_from_raw(raw) { 66 if $tinystr_ident.len() >= $len_start && $is_normalized { 67 Ok(Self($tinystr_ident)) 68 } else { 69 Err(crate::parser::errors::ParseError::$error) 70 } 71 } else { 72 Err(crate::parser::errors::ParseError::$error) 73 } 74 } 75 76 #[doc = concat!("Unsafely creates a [`", stringify!($name), "`] from its raw format")] 77 /// as returned by [`Self::into_raw`]. Unlike [`Self::try_from_utf8`], 78 /// this constructor only takes normalized values. 79 /// 80 /// # Safety 81 /// 82 /// This function is safe iff [`Self::try_from_raw`] returns an `Ok`. This is the case 83 /// for inputs that are correctly normalized. 84 pub const unsafe fn from_raw_unchecked(v: [u8; $len_end]) -> Self { 85 Self(tinystr::TinyAsciiStr::from_utf8_unchecked(v)) 86 } 87 88 /// Deconstructs into a raw format to be consumed by 89 /// [`from_raw_unchecked`](Self::from_raw_unchecked()) or 90 /// [`try_from_raw`](Self::try_from_raw()). 91 pub const fn into_raw(self) -> [u8; $len_end] { 92 *self.0.all_bytes() 93 } 94 95 #[inline] 96 /// A helper function for displaying as a `&str`. 97 pub const fn as_str(&self) -> &str { 98 self.0.as_str() 99 } 100 101 #[doc(hidden)] 102 pub const fn to_tinystr(&self) -> tinystr::TinyAsciiStr<$len_end> { 103 self.0 104 } 105 106 /// Compare with BCP-47 bytes. 107 /// 108 /// The return value is equivalent to what would happen if you first converted 109 /// `self` to a BCP-47 string and then performed a byte comparison. 110 /// 111 /// This function is case-sensitive and results in a *total order*, so it is appropriate for 112 /// binary search. The only argument producing [`Ordering::Equal`](core::cmp::Ordering::Equal) 113 /// is `self.as_str().as_bytes()`. 114 #[inline] 115 pub fn strict_cmp(self, other: &[u8]) -> core::cmp::Ordering { 116 self.as_str().as_bytes().cmp(other) 117 } 118 119 /// Compare with a potentially unnormalized BCP-47 string. 120 /// 121 /// The return value is equivalent to what would happen if you first parsed the 122 /// BCP-47 string and then performed a structural comparison. 123 /// 124 #[inline] 125 pub fn normalizing_eq(self, other: &str) -> bool { 126 self.as_str().eq_ignore_ascii_case(other) 127 } 128 } 129 130 impl core::str::FromStr for $name { 131 type Err = crate::parser::errors::ParseError; 132 133 #[inline] 134 fn from_str(s: &str) -> Result<Self, Self::Err> { 135 Self::try_from_str(s) 136 } 137 } 138 139 impl<'l> From<&'l $name> for &'l str { 140 fn from(input: &'l $name) -> Self { 141 input.as_str() 142 } 143 } 144 145 impl From<$name> for tinystr::TinyAsciiStr<$len_end> { 146 fn from(input: $name) -> Self { 147 input.to_tinystr() 148 } 149 } 150 151 impl writeable::Writeable for $name { 152 #[inline] 153 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result { 154 sink.write_str(self.as_str()) 155 } 156 #[inline] 157 fn writeable_length_hint(&self) -> writeable::LengthHint { 158 writeable::LengthHint::exact(self.0.len()) 159 } 160 #[inline] 161 #[cfg(feature = "alloc")] 162 fn write_to_string(&self) -> alloc::borrow::Cow<str> { 163 alloc::borrow::Cow::Borrowed(self.0.as_str()) 164 } 165 } 166 167 writeable::impl_display_with_writeable!($name); 168 169 #[doc = concat!("A macro allowing for compile-time construction of valid [`", stringify!($name), "`] subtags.")] 170 /// 171 /// # Examples 172 /// 173 /// Parsing errors don't have to be handled at runtime: 174 /// ``` 175 /// assert_eq!( 176 #[doc = concat!(" icu_locale_core::", $(stringify!($path), "::",)+ stringify!($macro_name), "!(", stringify!($good_example) ,"),")] 177 #[doc = concat!(" ", stringify!($good_example), ".parse::<icu_locale_core::", $(stringify!($path), "::",)+ stringify!($name), ">().unwrap()")] 178 /// ); 179 /// ``` 180 /// 181 /// Invalid input is a compile failure: 182 /// ```compile_fail,E0080 183 #[doc = concat!("icu_locale_core::", $(stringify!($path), "::",)+ stringify!($macro_name), "!(", stringify!($bad_example) ,");")] 184 /// ``` 185 /// 186 #[doc = concat!("[`", stringify!($name), "`]: crate::", $(stringify!($path), "::",)+ stringify!($name))] 187 #[macro_export] 188 #[doc(hidden)] // macro 189 macro_rules! $internal_macro_name { 190 ($string:literal) => {{ 191 use $crate::$($path ::)+ $name; 192 const R: $name = 193 match $name::try_from_utf8($string.as_bytes()) { 194 Ok(r) => r, 195 #[allow(clippy::panic)] // const context 196 _ => panic!(concat!("Invalid ", $(stringify!($path), "::",)+ stringify!($name), ": ", $string)), 197 }; 198 R 199 }}; 200 } 201 #[doc(inline)] 202 pub use $internal_macro_name as $macro_name; 203 204 #[cfg(feature = "databake")] 205 impl databake::Bake for $name { 206 fn bake(&self, env: &databake::CrateEnv) -> databake::TokenStream { 207 env.insert("icu_locale_core"); 208 let string = self.as_str(); 209 databake::quote! { icu_locale_core::$($path::)+ $macro_name!(#string) } 210 } 211 } 212 213 #[cfg(feature = "databake")] 214 impl databake::BakeSize for $name { 215 fn borrows_size(&self) -> usize { 216 0 217 } 218 } 219 220 #[test] 221 fn test_construction() { 222 let maybe = $name::try_from_utf8($good_example.as_bytes()); 223 assert!(maybe.is_ok()); 224 assert_eq!(maybe, $name::try_from_raw(maybe.unwrap().into_raw())); 225 assert_eq!(maybe.unwrap().as_str(), $good_example); 226 $( 227 let maybe = $name::try_from_utf8($more_good_examples.as_bytes()); 228 assert!(maybe.is_ok()); 229 assert_eq!(maybe, $name::try_from_raw(maybe.unwrap().into_raw())); 230 assert_eq!(maybe.unwrap().as_str(), $more_good_examples); 231 )* 232 assert!($name::try_from_utf8($bad_example.as_bytes()).is_err()); 233 $( 234 assert!($name::try_from_utf8($more_bad_examples.as_bytes()).is_err()); 235 )* 236 } 237 238 #[test] 239 fn test_writeable() { 240 writeable::assert_writeable_eq!(&$good_example.parse::<$name>().unwrap(), $good_example); 241 $( 242 writeable::assert_writeable_eq!($more_good_examples.parse::<$name>().unwrap(), $more_good_examples); 243 )* 244 } 245 246 #[cfg(feature = "serde")] 247 impl<'de> serde::Deserialize<'de> for $name { 248 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 249 where 250 D: serde::de::Deserializer<'de>, 251 { 252 struct Visitor; 253 254 impl<'de> serde::de::Visitor<'de> for Visitor { 255 type Value = $name; 256 257 fn expecting( 258 &self, 259 formatter: &mut core::fmt::Formatter<'_>, 260 ) -> core::fmt::Result { 261 write!(formatter, "a valid BCP-47 {}", stringify!($name)) 262 } 263 264 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> { 265 s.parse().map_err(serde::de::Error::custom) 266 } 267 } 268 269 if deserializer.is_human_readable() { 270 deserializer.deserialize_string(Visitor) 271 } else { 272 Self::try_from_raw(serde::de::Deserialize::deserialize(deserializer)?) 273 .map_err(serde::de::Error::custom) 274 } 275 } 276 } 277 278 // Safety checklist for ULE: 279 // 280 // 1. Must not include any uninitialized or padding bytes (true since transparent over a ULE). 281 // 2. Must have an alignment of 1 byte (true since transparent over a ULE). 282 // 3. ULE::validate_bytes() checks that the given byte slice represents a valid slice. 283 // 4. ULE::validate_bytes() checks that the given byte slice has a valid length. 284 // 5. All other methods must be left with their default impl. 285 // 6. Byte equality is semantic equality. 286 #[cfg(feature = "zerovec")] 287 unsafe impl zerovec::ule::ULE for $name { 288 fn validate_bytes(bytes: &[u8]) -> Result<(), zerovec::ule::UleError> { 289 let it = bytes.chunks_exact(core::mem::size_of::<Self>()); 290 if !it.remainder().is_empty() { 291 return Err(zerovec::ule::UleError::length::<Self>(bytes.len())); 292 } 293 for v in it { 294 // The following can be removed once `array_chunks` is stabilized. 295 let mut a = [0; core::mem::size_of::<Self>()]; 296 a.copy_from_slice(v); 297 if Self::try_from_raw(a).is_err() { 298 return Err(zerovec::ule::UleError::parse::<Self>()); 299 } 300 } 301 Ok(()) 302 } 303 } 304 305 #[cfg(feature = "zerovec")] 306 impl zerovec::ule::NicheBytes<$len_end> for $name { 307 const NICHE_BIT_PATTERN: [u8; $len_end] = <tinystr::TinyAsciiStr<$len_end>>::NICHE_BIT_PATTERN; 308 } 309 310 #[cfg(feature = "zerovec")] 311 impl zerovec::ule::AsULE for $name { 312 type ULE = Self; 313 fn to_unaligned(self) -> Self::ULE { 314 self 315 } 316 fn from_unaligned(unaligned: Self::ULE) -> Self { 317 unaligned 318 } 319 } 320 321 #[cfg(feature = "zerovec")] 322 impl<'a> zerovec::maps::ZeroMapKV<'a> for $name { 323 type Container = zerovec::ZeroVec<'a, $name>; 324 type Slice = zerovec::ZeroSlice<$name>; 325 type GetType = $name; 326 type OwnedType = $name; 327 } 328 }; 329 } 330 331 #[macro_export] 332 #[doc(hidden)] 333 macro_rules! impl_writeable_for_each_subtag_str_no_test { 334 ($type:tt $(, $self:ident, $borrow_cond:expr => $borrow:expr)?) => { 335 impl writeable::Writeable for $type { 336 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result { 337 let mut initial = true; 338 self.for_each_subtag_str(&mut |subtag| { 339 if initial { 340 initial = false; 341 } else { 342 sink.write_char('-')?; 343 } 344 sink.write_str(subtag) 345 }) 346 } 347 348 #[inline] 349 fn writeable_length_hint(&self) -> writeable::LengthHint { 350 let mut result = writeable::LengthHint::exact(0); 351 let mut initial = true; 352 self.for_each_subtag_str::<core::convert::Infallible, _>(&mut |subtag| { 353 if initial { 354 initial = false; 355 } else { 356 result += 1; 357 } 358 result += subtag.len(); 359 Ok(()) 360 }) 361 .expect("infallible"); 362 result 363 } 364 365 $( 366 #[cfg(feature = "alloc")] 367 fn write_to_string(&self) -> alloc::borrow::Cow<str> { 368 #[allow(clippy::unwrap_used)] // impl_writeable_for_subtag_list's $borrow uses unwrap 369 let $self = self; 370 if $borrow_cond { 371 $borrow 372 } else { 373 let mut output = alloc::string::String::with_capacity(self.writeable_length_hint().capacity()); 374 let _ = self.write_to(&mut output); 375 alloc::borrow::Cow::Owned(output) 376 } 377 } 378 )? 379 } 380 381 writeable::impl_display_with_writeable!($type); 382 }; 383 } 384 385 macro_rules! impl_writeable_for_subtag_list { 386 ($type:tt, $sample1:literal, $sample2:literal) => { 387 impl_writeable_for_each_subtag_str_no_test!($type, selff, selff.0.len() == 1 => alloc::borrow::Cow::Borrowed(selff.0.get(0).unwrap().as_str())); 388 389 #[test] 390 fn test_writeable() { 391 writeable::assert_writeable_eq!(&$type::default(), ""); 392 writeable::assert_writeable_eq!( 393 &$type::from_vec_unchecked(alloc::vec![$sample1.parse().unwrap()]), 394 $sample1, 395 ); 396 writeable::assert_writeable_eq!( 397 &$type::from_vec_unchecked(vec![ 398 $sample1.parse().unwrap(), 399 $sample2.parse().unwrap() 400 ]), 401 core::concat!($sample1, "-", $sample2), 402 ); 403 } 404 }; 405 } 406 407 macro_rules! impl_writeable_for_key_value { 408 ($type:tt, $key1:literal, $value1:literal, $key2:literal, $expected2:literal) => { 409 impl_writeable_for_each_subtag_str_no_test!($type); 410 411 #[test] 412 fn test_writeable() { 413 writeable::assert_writeable_eq!(&$type::default(), ""); 414 writeable::assert_writeable_eq!( 415 &$type::from_tuple_vec(vec![($key1.parse().unwrap(), $value1.parse().unwrap())]), 416 core::concat!($key1, "-", $value1), 417 ); 418 writeable::assert_writeable_eq!( 419 &$type::from_tuple_vec(vec![ 420 ($key1.parse().unwrap(), $value1.parse().unwrap()), 421 ($key2.parse().unwrap(), "true".parse().unwrap()) 422 ]), 423 core::concat!($key1, "-", $value1, "-", $expected2), 424 ); 425 } 426 }; 427 } 428