diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f1132c9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: rust -rust: - - 1.36.0 - - stable - - nightly -sudo: false -script: - - cargo build --verbose - - cargo test --verbose - - cargo test --verbose --no-default-features - - cargo package - - cd target/package/unicode-normalization-* - - cargo test --verbose - - cargo test --verbose --no-default-features -notifications: - email: - on_success: never diff --git a/Cargo.toml b/Cargo.toml index 3bea581..6eb512e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "unicode-normalization" -version = "0.1.24" +version = "0.2.0" authors = [ "kwantam ", "Manish Goregaokar ", @@ -27,9 +27,9 @@ Decomposition and Recomposition, as described in Unicode Standard Annex #15. """ -rust-version = "1.36" +rust-version = "1.84" -edition = "2018" +edition = "2021" exclude = ["target/*", "Cargo.lock", "scripts/tmp", "*.txt", "tests/*"] @@ -37,7 +37,6 @@ exclude = ["target/*", "Cargo.lock", "scripts/tmp", "*.txt", "tests/*"] version = "1" features = ["alloc"] - [features] default = ["std"] std = [] diff --git a/README.md b/README.md index 9139b47..1089a29 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # unicode-normalization -[![Build Status](https://travis-ci.org/unicode-rs/unicode-normalization.svg)](https://travis-ci.org/unicode-rs/unicode-normalization) [![Docs](https://docs.rs/unicode-normalization/badge.svg)](https://docs.rs/unicode-normalization/) Unicode character composition and decomposition utilities as described in [Unicode Standard Annex #15](http://www.unicode.org/reports/tr15/). -This crate requires Rust 1.36+. +This crate requires Rust 1.84+. ```rust extern crate unicode_normalization; @@ -31,21 +30,9 @@ to your `Cargo.toml`: ```toml [dependencies] -unicode-normalization = "0.1.24" +unicode-normalization = "0.2.0" ``` ## `no_std` + `alloc` support This crate is completely `no_std` + `alloc` compatible. This can be enabled by disabling the `std` feature, i.e. specifying `default-features = false` for this crate on your `Cargo.toml`. - -## Note about MSRV - -Dependencies' MSRVs evolve independently of this crate's MSRV. -Old versions of cargo will always try to get the most recent versions of the dependencies. -Therefore, if you are having troubles compiling on an old Rust version, try to install an older version of the incompatible dependency. - -For instance, to compile on Rust 1.36, `tinyvec` must be `<=1.6.0` - -```sh -cargo update -p tinyvec --precise 1.6.0 -``` diff --git a/fuzz/fuzz_targets/unicode-normalization.rs b/fuzz/fuzz_targets/unicode-normalization.rs index 7c99c7d..738511b 100644 --- a/fuzz/fuzz_targets/unicode-normalization.rs +++ b/fuzz/fuzz_targets/unicode-normalization.rs @@ -4,74 +4,66 @@ extern crate libfuzzer_sys; use unicode_normalization::{ - is_nfc, is_nfc_quick, is_nfc_stream_safe, is_nfc_stream_safe_quick, is_nfd, is_nfd_quick, - is_nfd_stream_safe, is_nfd_stream_safe_quick, is_nfkc, is_nfkc_quick, is_nfkd, is_nfkd_quick, - IsNormalized, UnicodeNormalization, + check_nfc_quick, check_nfc_stream_safe_quick, check_nfd_quick, check_nfd_stream_safe_quick, + check_nfkc_quick, check_nfkd_quick, is_nfc, is_nfc_stream_safe, is_nfd, is_nfd_stream_safe, + is_nfkc, is_nfkd, UnicodeNormalization, }; -fn from_bool(is_normalized: bool) -> IsNormalized { - if is_normalized { - IsNormalized::Yes - } else { - IsNormalized::No - } -} - fuzz_target!(|input: String| { // The full predicates imply the quick predicates. - assert_ne!(is_nfc_quick(input.chars()), from_bool(!is_nfc(&input))); - assert_ne!(is_nfd_quick(input.chars()), from_bool(!is_nfd(&input))); - assert_ne!(is_nfkc_quick(input.chars()), from_bool(!is_nfkc(&input))); - assert_ne!(is_nfkd_quick(input.chars()), from_bool(!is_nfkd(&input))); - assert_ne!( - is_nfc_stream_safe_quick(input.chars()), - from_bool(!is_nfc_stream_safe(&input)) + assert_eq!(check_nfc_quick(&input).is_ok(), is_nfc(&input)); + assert_eq!(check_nfd_quick(&input).is_ok(), is_nfd(&input)); + assert_eq!(check_nfkc_quick(&input).is_ok(), is_nfkc(&input)); + assert_eq!(check_nfkd_quick(&input).is_ok(), is_nfkd(&input)); + assert_eq!( + check_nfc_stream_safe_quick(&input).is_ok(), + is_nfc_stream_safe(&input) ); - assert_ne!( - is_nfd_stream_safe_quick(input.chars()), - from_bool(!is_nfd_stream_safe(&input)) + assert_eq!( + check_nfd_stream_safe_quick(&input).is_ok(), + is_nfd_stream_safe(&input) ); // Check NFC, NFD, NFKC, and NFKD normalization. - let nfc = input.chars().nfc().collect::(); + let nfc = input.nfc().collect::(); assert_eq!(nfc.is_empty(), input.is_empty()); - assert_ne!(is_nfc_quick(nfc.chars()), IsNormalized::No); + assert!(check_nfc_quick(&nfc).is_ok()); assert!(is_nfc(&nfc)); - let nfd = input.chars().nfd().collect::(); + let nfd = input.nfd().collect::(); assert!(nfd.len() >= nfc.len()); - assert_ne!(is_nfd_quick(nfd.chars()), IsNormalized::No); + assert!(check_nfd_quick(&nfd).is_ok()); assert!(is_nfd(&nfd)); - let nfkc = input.chars().nfkc().collect::(); + let nfkc = input.nfkc().collect::(); assert_eq!(nfkc.is_empty(), input.is_empty()); - assert_ne!(is_nfkc_quick(nfkc.chars()), IsNormalized::No); + assert!(check_nfkc_quick(&nfkc).is_ok()); assert!(is_nfkc(&nfkc)); - let nfkd = input.chars().nfkd().collect::(); + let nfkd = input.nfkd().collect::(); assert!(nfkd.len() >= nfkc.len()); - assert_ne!(is_nfkd_quick(nfkd.chars()), IsNormalized::No); + assert!(check_nfkd_quick(&nfkd).is_ok()); assert!(is_nfkd(&nfkd)); // Check stream-safe. - let nfc_ss = nfc.chars().stream_safe().collect::(); + let nfc_ss = nfc.stream_safe().collect::(); assert!(nfc_ss.len() >= nfc.len()); - assert_ne!(is_nfc_stream_safe_quick(nfc_ss.chars()), IsNormalized::No); + assert!(check_nfc_stream_safe_quick(&nfc_ss).is_ok()); assert!(is_nfc_stream_safe(&nfc_ss)); - let nfd_ss = nfd.chars().stream_safe().collect::(); + let nfd_ss = nfd.stream_safe().collect::(); assert!(nfd_ss.len() >= nfd.len()); - assert_ne!(is_nfd_stream_safe_quick(nfd_ss.chars()), IsNormalized::No); + assert!(check_nfd_stream_safe_quick(&nfd_ss).is_ok()); assert!(is_nfd_stream_safe(&nfd_ss)); // Check that NFC and NFD preserve stream-safe. - let ss_nfc = input.chars().stream_safe().nfc().collect::(); + let ss_nfc = input.stream_safe().nfc().collect::(); assert_eq!(ss_nfc.is_empty(), input.is_empty()); - assert_ne!(is_nfc_stream_safe_quick(ss_nfc.chars()), IsNormalized::No); + assert!(check_nfc_stream_safe_quick(&ss_nfc).is_ok()); assert!(is_nfc_stream_safe(&ss_nfc)); - let ss_nfd = input.chars().stream_safe().nfd().collect::(); + let ss_nfd = input.stream_safe().nfd().collect::(); assert_eq!(ss_nfd.is_empty(), input.is_empty()); - assert_ne!(is_nfd_stream_safe_quick(ss_nfd.chars()), IsNormalized::No); + assert!(check_nfd_stream_safe_quick(&ss_nfd).is_ok()); assert!(is_nfd_stream_safe(&ss_nfd)); }); diff --git a/scripts/unicode.py b/scripts/unicode.py index 0e5095c..6854640 100755 --- a/scripts/unicode.py +++ b/scripts/unicode.py @@ -463,7 +463,8 @@ def gen_public_assigned(general_category_public_assigned, out): # This could be done as a hash but the table is somewhat small. out.write("#[inline]\n") out.write("pub fn is_public_assigned(c: char) -> bool {\n") - out.write(" match c {\n") + out.write(" matches!(\n") + out.write(" c,\n") start = True for first, last in general_category_public_assigned: @@ -476,10 +477,9 @@ def gen_public_assigned(general_category_public_assigned, out): out.write("'\\u{%s}'" % hexify(first)) else: out.write("'\\u{%s}'..='\\u{%s}'" % (hexify(first), hexify(last))) - out.write(" => true,\n") + out.write(",\n") - out.write(" _ => false,\n") - out.write(" }\n") + out.write(" )\n") out.write("}\n") def gen_stream_safe(leading, trailing, out): diff --git a/src/lib.rs b/src/lib.rs index 963d41a..b4d5c54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ //! //! ```toml //! [dependencies] -//! unicode-normalization = "0.1.20" +#![doc = concat!("unicode-normalization = \"", env!("CARGO_PKG_VERSION"), "\"")] //! ``` #![deny(missing_docs, unsafe_code)] @@ -54,9 +54,10 @@ extern crate tinyvec; pub use crate::decompose::Decompositions; pub use crate::quick_check::{ - is_nfc, is_nfc_quick, is_nfc_stream_safe, is_nfc_stream_safe_quick, is_nfd, is_nfd_quick, - is_nfd_stream_safe, is_nfd_stream_safe_quick, is_nfkc, is_nfkc_quick, is_nfkd, is_nfkd_quick, - IsNormalized, + check_nfc, check_nfc_quick, check_nfc_stream_safe, check_nfc_stream_safe_quick, check_nfd, + check_nfd_quick, check_nfd_stream_safe, check_nfd_stream_safe_quick, check_nfkc, + check_nfkc_quick, check_nfkd, check_nfkd_quick, is_nfc, is_nfc_stream_safe, is_nfd, + is_nfd_stream_safe, is_nfkc, is_nfkd, NormalizationError, QuickCheck, }; pub use crate::recompose::Recompositions; pub use crate::replace::Replacements; diff --git a/src/quick_check.rs b/src/quick_check.rs index 728e341..cd9fcae 100644 --- a/src/quick_check.rs +++ b/src/quick_check.rs @@ -3,13 +3,46 @@ use crate::stream_safe; use crate::tables; use crate::UnicodeNormalization; -/// QuickCheck quickly determines if a string is normalized, it can return -/// `Maybe` +use core::error::Error; +use core::fmt; + +/// Error returned when a string is not properly normalized. +#[derive(Clone, Debug)] +pub struct NormalizationError { + /// String was normal up to this position. + normal_up_to: usize, +} +impl NormalizationError { + /// Returns the index in the given string up to which it was properly normalized. + pub const fn normal_up_to(&self) -> usize { + self.normal_up_to + } +} +impl fmt::Display for NormalizationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "string was not normalized at position {}", + self.normal_up_to + ) + } +} +impl Error for NormalizationError {} + +/// Whether additional checking is necessary to verify normalization. /// /// The QuickCheck algorithm can quickly determine if a text is or isn't /// normalized without any allocations in many cases, but it has to be able to /// return `Maybe` when a full decomposition and recomposition is necessary. #[derive(Debug, Eq, PartialEq)] +pub enum QuickCheck { + /// The text is definitely normalized. + Yes, + /// The text may be normalized. + Maybe, +} + +/// Normalization status of single character. pub enum IsNormalized { /// The text is definitely normalized. Yes, @@ -21,15 +54,19 @@ pub enum IsNormalized { // https://unicode.org/reports/tr15/#Detecting_Normalization_Forms #[inline] -fn quick_check(s: I, is_allowed: F, stream_safe: bool) -> IsNormalized +fn quick_check( + s: I, + is_allowed: F, + stream_safe: bool, +) -> Result where - I: Iterator, + I: Iterator, F: Fn(char) -> IsNormalized, { let mut last_cc = 0u8; let mut nonstarter_count = 0; - let mut result = IsNormalized::Yes; - for ch in s { + let mut result = QuickCheck::Yes; + for (idx, ch) in s { // For ASCII we know it's always allowed and a starter if ch <= '\x7f' { last_cc = 0; @@ -40,13 +77,13 @@ where // Otherwise, lookup the combining class and QC property let cc = canonical_combining_class(ch); if last_cc > cc && cc != 0 { - return IsNormalized::No; + return Err(NormalizationError { normal_up_to: idx }); } match is_allowed(ch) { IsNormalized::Yes => (), - IsNormalized::No => return IsNormalized::No, + IsNormalized::No => return Err(NormalizationError { normal_up_to: idx }), IsNormalized::Maybe => { - result = IsNormalized::Maybe; + result = QuickCheck::Maybe; } } if stream_safe { @@ -55,7 +92,7 @@ where // If we're above `MAX_NONSTARTERS`, we're definitely *not* // stream-safe normalized. if nonstarter_count + decomp.leading_nonstarters > stream_safe::MAX_NONSTARTERS { - return IsNormalized::No; + return Err(NormalizationError { normal_up_to: idx }); } if decomp.leading_nonstarters == decomp.decomposition_len { nonstarter_count += decomp.decomposition_len; @@ -65,126 +102,170 @@ where } last_cc = cc; } - result + Ok(result) +} + +fn full_check, J: Iterator>( + check: I, + normalized: J, +) -> Result<(), NormalizationError> { + check.zip(normalized).try_for_each(|((idx, lhs), rhs)| { + if lhs == rhs { + Ok(()) + } else { + Err(NormalizationError { normal_up_to: idx }) + } + }) } -/// Quickly check if a string is in NFC, potentially returning -/// `IsNormalized::Maybe` if further checks are necessary. In this case a check -/// like `s.chars().nfc().eq(s.chars())` should suffice. +/// Quickly check if a string is in NFC. #[inline] -pub fn is_nfc_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfc, false) +pub fn check_nfc_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfc, false) } /// Quickly check if a string is in NFKC. #[inline] -pub fn is_nfkc_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfkc, false) +pub fn check_nfkc_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfkc, false) } /// Quickly check if a string is in NFD. #[inline] -pub fn is_nfd_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfd, false) +pub fn check_nfd_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfd, false) } /// Quickly check if a string is in NFKD. #[inline] -pub fn is_nfkd_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfkd, false) +pub fn check_nfkd_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfkd, false) } /// Quickly check if a string is Stream-Safe NFC. #[inline] -pub fn is_nfc_stream_safe_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfc, true) +pub fn check_nfc_stream_safe_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfc, true) } /// Quickly check if a string is Stream-Safe NFD. #[inline] -pub fn is_nfd_stream_safe_quick>(s: I) -> IsNormalized { - quick_check(s, tables::qc_nfd, true) +pub fn check_nfd_stream_safe_quick(s: &str) -> Result { + quick_check(s.char_indices(), tables::qc_nfd, true) } /// Authoritatively check if a string is in NFC. #[inline] -pub fn is_nfc(s: &str) -> bool { - match is_nfc_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().nfc()), +pub fn check_nfc(s: &str) -> Result<(), NormalizationError> { + match check_nfc_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().nfc()), } } +/// Return whether a string is in NFC. +#[inline] +pub fn is_nfc(s: &str) -> bool { + check_nfc(s).is_ok() +} + /// Authoritatively check if a string is in NFKC. #[inline] -pub fn is_nfkc(s: &str) -> bool { - match is_nfkc_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().nfkc()), +pub fn check_nfkc(s: &str) -> Result<(), NormalizationError> { + match check_nfkc_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().nfkc()), } } +/// Return whether a string is in NFKC. +#[inline] +pub fn is_nfkc(s: &str) -> bool { + check_nfkc(s).is_ok() +} + /// Authoritatively check if a string is in NFD. #[inline] -pub fn is_nfd(s: &str) -> bool { - match is_nfd_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().nfd()), +pub fn check_nfd(s: &str) -> Result<(), NormalizationError> { + match check_nfd_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().nfd()), } } +/// Return whether a string is in NFD. +#[inline] +pub fn is_nfd(s: &str) -> bool { + check_nfd(s).is_ok() +} + /// Authoritatively check if a string is in NFKD. #[inline] -pub fn is_nfkd(s: &str) -> bool { - match is_nfkd_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().nfkd()), +pub fn check_nfkd(s: &str) -> Result<(), NormalizationError> { + match check_nfkd_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().nfkd()), } } +/// Return whether a string is in NFKD. +#[inline] +pub fn is_nfkd(s: &str) -> bool { + check_nfkd(s).is_ok() +} + /// Authoritatively check if a string is Stream-Safe NFC. #[inline] -pub fn is_nfc_stream_safe(s: &str) -> bool { - match is_nfc_stream_safe_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().stream_safe().nfc()), +pub fn check_nfc_stream_safe(s: &str) -> Result<(), NormalizationError> { + match check_nfc_stream_safe_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().stream_safe().nfc()), } } +/// Return whether a string is Stream-Safe NFC. +#[inline] +pub fn is_nfc_stream_safe(s: &str) -> bool { + check_nfc_stream_safe(s).is_ok() +} + /// Authoritatively check if a string is Stream-Safe NFD. #[inline] -pub fn is_nfd_stream_safe(s: &str) -> bool { - match is_nfd_stream_safe_quick(s.chars()) { - IsNormalized::Yes => true, - IsNormalized::No => false, - IsNormalized::Maybe => s.chars().eq(s.chars().stream_safe().nfd()), +pub fn check_nfd_stream_safe(s: &str) -> Result<(), NormalizationError> { + match check_nfd_stream_safe_quick(s)? { + QuickCheck::Yes => Ok(()), + QuickCheck::Maybe => full_check(s.char_indices(), s.chars().stream_safe().nfd()), } } +/// Return whether a string is Stream-Safe NFD. +#[inline] +pub fn is_nfd_stream_safe(s: &str) -> bool { + check_nfd_stream_safe(s).is_ok() +} + #[cfg(test)] mod tests { - use super::{is_nfc_stream_safe_quick, is_nfd_stream_safe_quick, IsNormalized}; + use super::{check_nfc_stream_safe_quick, check_nfd_stream_safe_quick, QuickCheck}; #[test] fn test_stream_safe_nfd() { let okay = "Da\u{031b}\u{0316}\u{0317}\u{0318}\u{0319}\u{031c}\u{031d}\u{0300}\u{0301}\u{0302}\u{0303}\u{0304}\u{0305}\u{0306}\u{0307}\u{0308}\u{0309}\u{030a}\u{030b}\u{030c}\u{030d}\u{030e}\u{030f}\u{0310}\u{0311}\u{0312}\u{0313}\u{0314}\u{0315}\u{031a}ngerzone"; - assert_eq!(is_nfd_stream_safe_quick(okay.chars()), IsNormalized::Yes); + assert_eq!(check_nfd_stream_safe_quick(okay).unwrap(), QuickCheck::Yes); let too_much = "Da\u{031b}\u{0316}\u{0317}\u{0318}\u{0319}\u{031c}\u{031d}\u{031e}\u{0300}\u{0301}\u{0302}\u{0303}\u{0304}\u{0305}\u{0306}\u{0307}\u{0308}\u{0309}\u{030a}\u{030b}\u{030c}\u{030d}\u{030e}\u{030f}\u{0310}\u{0311}\u{0312}\u{0313}\u{0314}\u{0315}\u{031a}ngerzone"; - assert_eq!(is_nfd_stream_safe_quick(too_much.chars()), IsNormalized::No); + assert!(check_nfd_stream_safe_quick(too_much).is_err()); } #[test] fn test_stream_safe_nfc() { let okay = "ok\u{e0}\u{031b}\u{0316}\u{0317}\u{0318}\u{0319}\u{031c}\u{031d}\u{0301}\u{0302}\u{0303}\u{0304}\u{0305}\u{0306}\u{0307}\u{0308}\u{0309}\u{030a}\u{030b}\u{030c}\u{030d}\u{030e}\u{030f}\u{0310}\u{0311}\u{0312}\u{0313}\u{0314}\u{0315}\u{031a}y"; - assert_eq!(is_nfc_stream_safe_quick(okay.chars()), IsNormalized::Maybe); + assert_eq!( + check_nfc_stream_safe_quick(okay).unwrap(), + QuickCheck::Maybe + ); let too_much = "not ok\u{e0}\u{031b}\u{0316}\u{0317}\u{0318}\u{0319}\u{031c}\u{031d}\u{031e}\u{0301}\u{0302}\u{0303}\u{0304}\u{0305}\u{0306}\u{0307}\u{0308}\u{0309}\u{030a}\u{030b}\u{030c}\u{030d}\u{030e}\u{030f}\u{0310}\u{0311}\u{0312}\u{0313}\u{0314}\u{0315}\u{031a}y"; - assert_eq!(is_nfc_stream_safe_quick(too_much.chars()), IsNormalized::No); + assert!(check_nfc_stream_safe_quick(too_much).is_err()); } } diff --git a/src/tables.rs b/src/tables.rs index 4bbff63..ed879ec 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -20433,7 +20433,8 @@ pub(crate) const COMBINING_MARK_KV: &[u32] = &[ ]; #[inline] pub fn is_public_assigned(c: char) -> bool { - match c { + matches!( + c, '\u{0000}'..='\u{0377}' | '\u{037A}'..='\u{037F}' | '\u{0384}'..='\u{038A}' @@ -21166,9 +21167,8 @@ pub fn is_public_assigned(c: char) -> bool { | '\u{31350}'..='\u{33479}' | '\u{E0001}' | '\u{E0020}'..='\u{E007F}' - | '\u{E0100}'..='\u{E01EF}' => true, - _ => false, - } + | '\u{E0100}'..='\u{E01EF}', + ) } #[inline]