Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
//! An implementation of the "stringprep" algorithm defined in [RFC 3454][].
//!
//! [RFC 3454]: https://tools.ietf.org/html/rfc3454
#![warn(missing_docs)]
extern crate unicode_bidi;
extern crate unicode_normalization;
extern crate unicode_properties;
use std::borrow::Cow;
use std::fmt;
use unicode_normalization::UnicodeNormalization;
use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory};
mod rfc3454;
pub mod tables;
/// Describes why a string failed stringprep normalization.
#[derive(Debug)]
enum ErrorCause {
/// Contains stringprep prohibited characters.
ProhibitedCharacter(char),
/// Violates stringprep rules for bidirectional text.
ProhibitedBidirectionalText,
/// Starts with a combining character
StartsWithCombiningCharacter,
/// Empty String
EmptyString,
}
/// An error performing the stringprep algorithm.
#[derive(Debug)]
pub struct Error(ErrorCause);
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
ErrorCause::ProhibitedCharacter(c) => write!(fmt, "prohibited character `{}`", c),
ErrorCause::ProhibitedBidirectionalText => write!(fmt, "prohibited bidirectional text"),
ErrorCause::StartsWithCombiningCharacter => {
write!(fmt, "starts with combining character")
}
ErrorCause::EmptyString => write!(fmt, "empty string"),
}
}
}
impl std::error::Error for Error {}
/// Prepares a string with the SASLprep profile of the stringprep algorithm.
///
/// SASLprep is defined in [RFC 4013][].
///
/// [RFC 4013]: https://tools.ietf.org/html/rfc4013
pub fn saslprep(s: &str) -> Result<Cow<'_, str>, Error> {
// fast path for ascii text
if s.chars()
.all(|c| c.is_ascii() && !tables::ascii_control_character(c))
{
return Ok(Cow::Borrowed(s));
}
// 2.1 Mapping
let mapped = s
.chars()
.map(|c| {
if tables::non_ascii_space_character(c) {
' '
} else {
c
}
})
.filter(|&c| !tables::commonly_mapped_to_nothing(c));
// 2.2 Normalization
let normalized = mapped.nfkc().collect::<String>();
// 2.3 Prohibited Output
let prohibited = normalized.chars().find(|&c| {
tables::non_ascii_space_character(c) /* C.1.2 */ ||
tables::ascii_control_character(c) /* C.2.1 */ ||
tables::non_ascii_control_character(c) /* C.2.2 */ ||
tables::private_use(c) /* C.3 */ ||
tables::non_character_code_point(c) /* C.4 */ ||
tables::surrogate_code(c) /* C.5 */ ||
tables::inappropriate_for_plain_text(c) /* C.6 */ ||
tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
tables::change_display_properties_or_deprecated(c) /* C.8 */ ||
tables::tagging_character(c) /* C.9 */
});
if let Some(c) = prohibited {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
// 2.4. Bidirectional Characters
if is_prohibited_bidirectional_text(&normalized) {
return Err(Error(ErrorCause::ProhibitedBidirectionalText));
}
// 2.5 Unassigned Code Points
let unassigned = normalized
.chars()
.find(|&c| tables::unassigned_code_point(c));
if let Some(c) = unassigned {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
Ok(Cow::Owned(normalized))
}
// RFC3454, 6. Bidirectional Characters
fn is_prohibited_bidirectional_text(s: &str) -> bool {
if s.contains(tables::bidi_r_or_al) {
// 2) If a string contains any RandALCat character, the string
// MUST NOT contain any LCat character.
if s.contains(tables::bidi_l) {
return true;
}
// 3) If a string contains any RandALCat character, a RandALCat
// character MUST be the first character of the string, and a
// RandALCat character MUST be the last character of the string.
if !tables::bidi_r_or_al(s.chars().next().unwrap())
|| !tables::bidi_r_or_al(s.chars().next_back().unwrap())
{
return true;
}
}
false
}
/// Prepares a string with the Nameprep profile of the stringprep algorithm.
///
/// Nameprep is defined in [RFC 3491][].
///
/// [RFC 3491]: https://tools.ietf.org/html/rfc3491
pub fn nameprep(s: &str) -> Result<Cow<'_, str>, Error> {
// fast path for ascii text
if s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '-')
{
return Ok(Cow::Borrowed(s));
}
// 3. Mapping
let mapped = s
.chars()
.filter(|&c| !tables::commonly_mapped_to_nothing(c))
.flat_map(tables::case_fold_for_nfkc);
// 4. Normalization
let normalized = mapped.nfkc().collect::<String>();
// 5. Prohibited Output
let prohibited = normalized.chars().find(|&c| {
tables::non_ascii_space_character(c) /* C.1.2 */ ||
tables::non_ascii_control_character(c) /* C.2.2 */ ||
tables::private_use(c) /* C.3 */ ||
tables::non_character_code_point(c) /* C.4 */ ||
tables::surrogate_code(c) /* C.5 */ ||
tables::inappropriate_for_plain_text(c) /* C.6 */ ||
tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
tables::tagging_character(c) /* C.9 */
});
if let Some(c) = prohibited {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
// 6. Bidirectional Characters
if is_prohibited_bidirectional_text(&normalized) {
return Err(Error(ErrorCause::ProhibitedBidirectionalText));
}
// 7 Unassigned Code Points
let unassigned = normalized
.chars()
.find(|&c| tables::unassigned_code_point(c));
if let Some(c) = unassigned {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
Ok(Cow::Owned(normalized))
}
/// Prepares a string with the Nodeprep profile of the stringprep algorithm.
///
/// Nameprep is defined in [RFC 3920, Appendix A][].
///
/// [RFC 3920, Appendix A]: https://tools.ietf.org/html/rfc3920#appendix-A
pub fn nodeprep(s: &str) -> Result<Cow<'_, str>, Error> {
// fast path for common ascii text
if s.chars()
.all(|c| matches!(c, '['..='~' | '0'..='9' | '('..='.' | '#'..='%'))
{
return Ok(Cow::Borrowed(s));
}
// A.3. Mapping
let mapped = s
.chars()
.filter(|&c| !tables::commonly_mapped_to_nothing(c))
.flat_map(tables::case_fold_for_nfkc);
// A.4. Normalization
let normalized = mapped.nfkc().collect::<String>();
// A.5. Prohibited Output
let prohibited = normalized.chars().find(|&c| {
tables::ascii_space_character(c) /* C.1.1 */ ||
tables::non_ascii_space_character(c) /* C.1.2 */ ||
tables::ascii_control_character(c) /* C.2.1 */ ||
tables::non_ascii_control_character(c) /* C.2.2 */ ||
tables::private_use(c) /* C.3 */ ||
tables::non_character_code_point(c) /* C.4 */ ||
tables::surrogate_code(c) /* C.5 */ ||
tables::inappropriate_for_plain_text(c) /* C.6 */ ||
tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
tables::tagging_character(c) /* C.9 */ ||
prohibited_node_character(c)
});
if let Some(c) = prohibited {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
// A.6. Bidirectional Characters
if is_prohibited_bidirectional_text(&normalized) {
return Err(Error(ErrorCause::ProhibitedBidirectionalText));
}
let unassigned = normalized
.chars()
.find(|&c| tables::unassigned_code_point(c));
if let Some(c) = unassigned {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
Ok(Cow::Owned(normalized))
}
// Additional characters not allowed in JID nodes, by RFC3920.
fn prohibited_node_character(c: char) -> bool {
matches!(c, '"' | '&' | '\'' | '/' | ':' | '<' | '>' | '@')
}
/// Prepares a string with the Resourceprep profile of the stringprep algorithm.
///
/// Nameprep is defined in [RFC 3920, Appendix B][].
///
/// [RFC 3920, Appendix B]: https://tools.ietf.org/html/rfc3920#appendix-B
pub fn resourceprep(s: &str) -> Result<Cow<'_, str>, Error> {
// fast path for ascii text
if s.chars().all(|c| matches!(c, ' '..='~')) {
return Ok(Cow::Borrowed(s));
}
// B.3. Mapping
let mapped = s
.chars()
.filter(|&c| !tables::commonly_mapped_to_nothing(c))
.collect::<String>();
// B.4. Normalization
let normalized = mapped.nfkc().collect::<String>();
// B.5. Prohibited Output
let prohibited = normalized.chars().find(|&c| {
tables::non_ascii_space_character(c) /* C.1.2 */ ||
tables::ascii_control_character(c) /* C.2.1 */ ||
tables::non_ascii_control_character(c) /* C.2.2 */ ||
tables::private_use(c) /* C.3 */ ||
tables::non_character_code_point(c) /* C.4 */ ||
tables::surrogate_code(c) /* C.5 */ ||
tables::inappropriate_for_plain_text(c) /* C.6 */ ||
tables::inappropriate_for_canonical_representation(c) /* C.7 */ ||
tables::change_display_properties_or_deprecated(c) /* C.9 */ ||
tables::tagging_character(c) /* C.9 */
});
if let Some(c) = prohibited {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
// B.6. Bidirectional Characters
if is_prohibited_bidirectional_text(&normalized) {
return Err(Error(ErrorCause::ProhibitedBidirectionalText));
}
let unassigned = normalized
.chars()
.find(|&c| tables::unassigned_code_point(c));
if let Some(c) = unassigned {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
Ok(Cow::Owned(normalized))
}
/// Prepares a string according to the procedures described in Section 7 of
/// [ITU-T Recommendation X.520 (2019)](https://www.itu.int/rec/T-REC-X.520-201910-I/en).
///
/// Note that this function does _not_ remove leading, trailing, or inner
/// spaces as described in Section 7.6, because the characters needing removal
/// will vary across the matching rules and ASN.1 syntaxes used.
pub fn x520prep(s: &str, case_fold: bool) -> Result<Cow<'_, str>, Error> {
if s.is_empty() {
return Err(Error(ErrorCause::EmptyString));
}
if s.chars()
.all(|c| matches!(c, ' '..='~') && (!case_fold || c.is_ascii_lowercase()))
{
return Ok(Cow::Borrowed(s));
}
// 1. Transcode
// Already done because &str is enforced to be Unicode.
// 2. Map
let mapped = s
.chars()
.filter(|&c| !tables::x520_mapped_to_nothing(c))
.map(|c| {
if tables::x520_mapped_to_space(c) {
' '
} else {
c
}
});
// 3. Normalize
let normalized = if case_fold {
mapped
.flat_map(tables::case_fold_for_nfkc)
.collect::<String>()
} else {
mapped.nfkc().collect::<String>()
};
// 4. Prohibit
let prohibited = normalized.chars().find(
|&c| {
tables::unassigned_code_point(c)
|| tables::private_use(c)
|| tables::non_character_code_point(c)
|| tables::surrogate_code(c)
|| c == '\u{FFFD}'
}, // REPLACEMENT CHARACTER
);
if let Some(c) = prohibited {
return Err(Error(ErrorCause::ProhibitedCharacter(c)));
}
// From ITU-T Recommendation X.520, Section 7.4:
// "The first code point of a string is prohibited from being a combining character."
match s.chars().next() {
Some(c) => {
if c.general_category_group() == GeneralCategoryGroup::Mark {
return Err(Error(ErrorCause::StartsWithCombiningCharacter));
}
}
None => return Err(Error(ErrorCause::EmptyString)),
}
// 5. Check bidi
// From ITU-T Recommendation X.520, Section 7.4:
// "There are no bidirectional restrictions. The output string is the input string."
// So there is nothing to do for this step.
// 6. Insignificant Character Removal
// Done in calling functions.
Ok(normalized.into())
}
#[cfg(test)]
mod test {
use super::*;
fn assert_prohibited_character<T>(result: Result<T, Error>) {
match result {
Err(Error(ErrorCause::ProhibitedCharacter(_))) => (),
_ => panic!(),
}
}
fn assert_starts_with_combining_char<T>(result: Result<T, Error>) {
match result {
Err(Error(ErrorCause::StartsWithCombiningCharacter)) => (),
_ => panic!(),
}
}
// RFC4013, 3. Examples
#[test]
fn saslprep_examples() {
assert_prohibited_character(saslprep("\u{0007}"));
}
#[test]
fn nodeprep_examples() {
assert_prohibited_character(nodeprep(" "));
assert_prohibited_character(nodeprep("\u{00a0}"));
assert_prohibited_character(nodeprep("foo@bar"));
}
#[test]
fn resourceprep_examples() {
assert_eq!("foo@bar", resourceprep("foo@bar").unwrap());
}
#[test]
fn x520prep_examples() {
assert_eq!(x520prep("foo@bar", true).unwrap(), "foo@bar");
assert_eq!(
x520prep("J.\u{FE00} \u{9}W. \u{B}wuz h\u{0115}re", false).unwrap(),
"J. W. wuz h\u{0115}re"
);
assert_eq!(
x520prep("J.\u{FE00} \u{9}W. \u{B}wuz h\u{0115}re", true).unwrap(),
"j. w. wuz h\u{0115}re"
);
assert_eq!(x520prep("UPPERCASED", true).unwrap(), "uppercased");
assert_starts_with_combining_char(x520prep("\u{0306}hello", true));
}
#[test]
fn ascii_optimisations() {
if let Cow::Owned(_) = nodeprep("nodepart").unwrap() {
panic!("“nodepart” should get optimised as ASCII");
}
if let Cow::Owned(_) = nameprep("domainpart.example").unwrap() {
panic!("“domainpart.example” should get optimised as ASCII");
}
if let Cow::Owned(_) = resourceprep("resourcepart").unwrap() {
panic!("“resourcepart” should get optimised as ASCII");
}
}
}
File diff suppressed because it is too large Load Diff
+251
View File
@@ -0,0 +1,251 @@
//! Character Tables
use std::cmp::Ordering;
use std::str::Chars;
use unicode_bidi::{bidi_class, BidiClass};
use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory};
use super::rfc3454;
/// A.1 Unassigned code points in Unicode 3.2
pub fn unassigned_code_point(c: char) -> bool {
rfc3454::A_1
.binary_search_by(|&(start, end)| {
if start > c {
Ordering::Greater
} else if end < c {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
}
/// B.1 Commonly mapped to nothing
pub fn commonly_mapped_to_nothing(c: char) -> bool {
matches!(
c,
'\u{00AD}'
| '\u{034F}'
| '\u{1806}'
| '\u{180B}'
| '\u{180C}'
| '\u{180D}'
| '\u{200B}'
| '\u{200C}'
| '\u{200D}'
| '\u{2060}'
| '\u{FE00}'
| '\u{FE01}'
| '\u{FE02}'
| '\u{FE03}'
| '\u{FE04}'
| '\u{FE05}'
| '\u{FE06}'
| '\u{FE07}'
| '\u{FE08}'
| '\u{FE09}'
| '\u{FE0A}'
| '\u{FE0B}'
| '\u{FE0C}'
| '\u{FE0D}'
| '\u{FE0E}'
| '\u{FE0F}'
| '\u{FEFF}'
)
}
/// B.2 Mapping for case-folding used with NFKC.
pub fn case_fold_for_nfkc(c: char) -> CaseFoldForNfkc {
let inner = match rfc3454::B_2.binary_search_by_key(&c, |e| e.0) {
Ok(idx) => FoldInner::Chars(rfc3454::B_2[idx].1.chars()),
Err(_) => FoldInner::Char(Some(c)),
};
CaseFoldForNfkc(inner)
}
enum FoldInner {
Chars(Chars<'static>),
Char(Option<char>),
}
/// The iterator returned by `case_fold_for_nfkc`.
pub struct CaseFoldForNfkc(FoldInner);
impl Iterator for CaseFoldForNfkc {
type Item = char;
fn next(&mut self) -> Option<char> {
match self.0 {
FoldInner::Chars(ref mut it) => it.next(),
FoldInner::Char(ref mut ch) => ch.take(),
}
}
}
/// C.1.1 ASCII space characters
pub fn ascii_space_character(c: char) -> bool {
c == ' '
}
/// C.1.2 Non-ASCII space characters
pub fn non_ascii_space_character(c: char) -> bool {
matches!(
c,
'\u{00A0}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200A}'
| '\u{200B}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
)
}
/// C.2.1 ASCII control characters
pub fn ascii_control_character(c: char) -> bool {
matches!(c, '\u{0000}'..='\u{001F}' | '\u{007F}')
}
/// C.2.2 Non-ASCII control characters
pub fn non_ascii_control_character(c: char) -> bool {
matches!(c, '\u{0080}'..='\u{009F}'
| '\u{06DD}'
| '\u{070F}'
| '\u{180E}'
| '\u{200C}'
| '\u{200D}'
| '\u{2028}'
| '\u{2029}'
| '\u{2060}'
| '\u{2061}'
| '\u{2062}'
| '\u{2063}'
| '\u{206A}'..='\u{206F}'
| '\u{FEFF}'
| '\u{FFF9}'..='\u{FFFC}'
| '\u{1D173}'..='\u{1D17A}')
}
/// C.3 Private use
pub fn private_use(c: char) -> bool {
matches!(c, '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}')
}
/// C.4 Non-character code points
pub fn non_character_code_point(c: char) -> bool {
matches!(c, '\u{FDD0}'..='\u{FDEF}'
| '\u{FFFE}'..='\u{FFFF}'
| '\u{1FFFE}'..='\u{1FFFF}'
| '\u{2FFFE}'..='\u{2FFFF}'
| '\u{3FFFE}'..='\u{3FFFF}'
| '\u{4FFFE}'..='\u{4FFFF}'
| '\u{5FFFE}'..='\u{5FFFF}'
| '\u{6FFFE}'..='\u{6FFFF}'
| '\u{7FFFE}'..='\u{7FFFF}'
| '\u{8FFFE}'..='\u{8FFFF}'
| '\u{9FFFE}'..='\u{9FFFF}'
| '\u{AFFFE}'..='\u{AFFFF}'
| '\u{BFFFE}'..='\u{BFFFF}'
| '\u{CFFFE}'..='\u{CFFFF}'
| '\u{DFFFE}'..='\u{DFFFF}'
| '\u{EFFFE}'..='\u{EFFFF}'
| '\u{FFFFE}'..='\u{FFFFF}'
| '\u{10FFFE}'..='\u{10FFFF}')
}
/// C.5 Surrogate codes
#[allow(clippy::match_single_binding)]
pub fn surrogate_code(c: char) -> bool {
match c {
// forbidden by rust
/*'\u{D800}'..='\u{DFFF}' => true,*/
_ => false,
}
}
/// C.6 Inappropriate for plain text
pub fn inappropriate_for_plain_text(c: char) -> bool {
matches!(
c,
'\u{FFF9}' | '\u{FFFA}' | '\u{FFFB}' | '\u{FFFC}' | '\u{FFFD}'
)
}
/// C.7 Inappropriate for canonical representation
pub fn inappropriate_for_canonical_representation(c: char) -> bool {
matches!(c, '\u{2FF0}'..='\u{2FFB}')
}
/// C.8 Change display properties or are deprecated
pub fn change_display_properties_or_deprecated(c: char) -> bool {
matches!(
c,
'\u{0340}'
| '\u{0341}'
| '\u{200E}'
| '\u{200F}'
| '\u{202A}'
| '\u{202B}'
| '\u{202C}'
| '\u{202D}'
| '\u{202E}'
| '\u{206A}'
| '\u{206B}'
| '\u{206C}'
| '\u{206D}'
| '\u{206E}'
| '\u{206F}'
)
}
/// C.9 Tagging characters
pub fn tagging_character(c: char) -> bool {
matches!(c, '\u{E0001}' | '\u{E0020}'..='\u{E007F}')
}
/// D.1 Characters with bidirectional property "R" or "AL"
pub fn bidi_r_or_al(c: char) -> bool {
matches!(bidi_class(c), BidiClass::R | BidiClass::AL)
}
/// D.2 Characters with bidirectional property "L"
pub fn bidi_l(c: char) -> bool {
matches!(bidi_class(c), BidiClass::L)
}
/// Determines if `c` is to be removed according to section 7.2 of
/// [ITU-T Recommendation X.520 (2019)](https://www.itu.int/rec/T-REC-X.520-201910-I/en).
pub fn x520_mapped_to_nothing(c: char) -> bool {
match c {
'\u{00AD}'
| '\u{1806}'
| '\u{034F}'
| '\u{180B}'..='\u{180D}'
| '\u{FE00}'..='\u{FE0F}'
| '\u{FFFC}'
| '\u{200B}' => true,
// Technically control characters, but mapped to whitespace in X.520.
'\u{09}' | '\u{0A}'..='\u{0D}' | '\u{85}' => false,
_ => c.is_control(),
}
}
/// Determines if `c` is to be replaced by SPACE (0x20) according to section 7.2 of
/// [ITU-T Recommendation X.520 (2019)](https://www.itu.int/rec/T-REC-X.520-201910-I/en).
pub fn x520_mapped_to_space(c: char) -> bool {
match c {
'\u{09}' | '\u{0A}'..='\u{0D}' | '\u{85}' => true,
_ => c.general_category_group() == GeneralCategoryGroup::Separator,
}
}