use o3::alloc::VecExt as _;
use std::{mem, ptr};
use stringprep::tables;
use unicode_normalization::char::{canonical_combining_class, compose, decompose_compatible};
use zeroize::Zeroizing;
mod bidi;
pub(crate) struct Password(Zeroizing<String>);
struct Decomposed(Zeroizing<Vec<char>>);
struct Composed(Zeroizing<Vec<char>>);
impl Password {
pub(crate) fn new(password: String) -> Self {
Self(Zeroizing::new(password))
}
pub(crate) fn try_prepare(mut self) -> Result<Self, crate::ConfigError> {
if self
.0
.chars()
.all(|ch| ch.is_ascii() && !tables::ascii_control_character(ch))
{
return Ok(self);
}
prepare(&mut self.0)?;
Ok(self)
}
pub(crate) fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub(crate) fn as_str(&self) -> &str {
self.0.as_str()
}
}
fn prepare(value: &mut Zeroizing<String>) -> Result<(), crate::ConfigError> {
let mut chars = mapped(value);
let Some(first) = chars.next() else {
return Ok(());
};
let mut last_right = false;
let mut right = false;
let mut left = false;
let mut length = Some(0usize);
let mut start = 0;
let mut last = 0;
let mut disordered = false;
let mut scratch = 0;
for ch in std::iter::once(first).chain(chars) {
if is_prohibited(ch) || tables::unassigned_code_point(ch) {
return Ok(());
}
last_right = bidi::right(ch);
right |= last_right;
left |= bidi::left(ch);
decompose_compatible(ch, |ch| {
length = length.and_then(|len| {
let class = canonical_combining_class(ch);
if class == 0 {
if disordered {
scratch = scratch.max(len - start);
}
start = len;
disordered = false;
} else {
disordered |= last > class;
}
last = class;
len.checked_add(1)
});
});
}
if right && (left || !bidi::right(first) || !last_right) {
return Ok(());
}
let length = length.ok_or(crate::ConfigError::PasswordCapacity)?;
if disordered {
scratch = scratch.max(length - start);
}
let mut source = Decomposed(Zeroizing::new(
Vec::try_new(length.saturating_add(scratch)).map_err(crate::ConfigError::Allocation)?,
));
for mapped in mapped(value) {
decompose_compatible(mapped, |ch| {
let output = &mut source.0;
let len = output.len();
unsafe {
output.as_mut_ptr().add(len).write(ch);
output.set_len(len + 1);
}
});
}
encode(&compose_all(source), value)
}
fn mapped(value: &str) -> impl Iterator<Item = char> + '_ {
value
.chars()
.map(|ch| {
if tables::non_ascii_space_character(ch) {
' '
} else {
ch
}
})
.filter(|ch| !tables::commonly_mapped_to_nothing(*ch))
}
fn compose_all(mut input: Decomposed) -> Composed {
let len = input.0.len();
let ordered_all = input.0.capacity() == len;
let values = input.0.as_mut_ptr();
let scratch = unsafe { values.add(len) };
let mut written = 0;
let mut starter: Option<*mut char> = None;
let mut previous = 0;
let mut start = 0;
while start < len {
let mut end = if ordered_all { len } else { start };
let mut last = 0;
let mut ordered = true;
while end < len {
let class = canonical_combining_class(unsafe { values.add(end).read() });
if class == 0 && end != start {
break;
}
ordered &= last <= class;
last = class;
end += 1;
}
let source = if ordered {
unsafe { values.add(start) }
} else {
let mut positions = Zeroizing::new([0usize; 256]);
for index in start..end {
let ch = unsafe { values.add(index).read() };
positions[canonical_combining_class(ch) as usize] += 1;
}
let mut offset = 0;
for count in positions.iter_mut() {
let next = offset + *count;
*count = offset;
offset = next;
}
for index in start..end {
let ch = unsafe { values.add(index).read() };
let at = &mut positions[canonical_combining_class(ch) as usize];
unsafe { scratch.add(*at).write(ch) };
*at += 1;
}
scratch
};
for index in 0..end - start {
let ch = unsafe { source.add(index).read() };
let class = canonical_combining_class(ch);
if let Some(current) = starter
&& (previous == 0 || previous < class)
&& let Some(joined) = compose(unsafe { current.read() }, ch)
{
unsafe { current.write(joined) };
continue;
}
let current = unsafe { values.add(written) };
unsafe { current.write(ch) };
if class == 0 {
starter = Some(current);
}
previous = class;
written += 1;
}
start = end;
}
input.0.truncate(written);
Composed(input.0)
}
fn is_prohibited(ch: char) -> bool {
tables::ascii_control_character(ch)
|| tables::non_ascii_control_character(ch)
|| tables::private_use(ch)
|| tables::non_character_code_point(ch)
|| tables::surrogate_code(ch)
|| tables::inappropriate_for_plain_text(ch)
|| tables::inappropriate_for_canonical_representation(ch)
|| tables::change_display_properties_or_deprecated(ch)
|| tables::tagging_character(ch)
}
fn encode(value: &Composed, original: &mut Zeroizing<String>) -> Result<(), crate::ConfigError> {
let bytes = value.0.iter().map(|ch| ch.len_utf8()).sum();
let mut output = if original.capacity() == bytes {
mem::take(&mut **original).into_bytes()
} else {
Vec::try_new(bytes).map_err(crate::ConfigError::Allocation)?
};
let mut cursor = output.as_mut_ptr();
let mut encoded = Zeroizing::new([0; 4]);
for ch in value.0.iter() {
let encoded = ch.encode_utf8(&mut encoded[..]);
unsafe {
ptr::copy_nonoverlapping(encoded.as_ptr(), cursor, encoded.len());
cursor = cursor.add(encoded.len());
}
}
unsafe {
output.set_len(bytes);
*original = Zeroizing::new(String::from_utf8_unchecked(output));
}
Ok(())
}