use std::error::Error;
use std::fmt;
use std::fmt::Write as _;
use std::ops::Range;
use std::sync::Arc;
#[derive(Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EcmaString(Arc<[u16]>);
impl EcmaString {
#[must_use]
pub fn from_utf8(value: &str) -> Self {
Self(Arc::from(value.encode_utf16().collect::<Vec<_>>()))
}
#[must_use]
pub fn from_units(units: &[u16]) -> Self {
Self(Arc::from(units))
}
#[must_use]
pub(crate) fn from_le_bytes(bytes: &[u8]) -> Self {
debug_assert!(bytes.len().is_multiple_of(2));
Self(
bytes
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect::<Arc<[u16]>>(),
)
}
#[must_use]
pub fn as_units(&self) -> &[u16] {
&self.0
}
#[must_use]
pub fn len_units(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn unit_at(&self, offset: usize) -> Option<u16> {
self.0.get(offset).copied()
}
#[must_use]
pub fn slice_units(&self, range: Range<usize>) -> Self {
Self::from_units(&self.0[range])
}
#[must_use]
pub fn is_well_formed(&self) -> bool {
self.first_ill_formed_offset().is_none()
}
#[must_use]
pub fn eq_ascii(&self, value: &str) -> bool {
value.is_ascii()
&& self.0.len() == value.len()
&& self
.0
.iter()
.zip(value.bytes())
.all(|(&unit, byte)| unit == u16::from(byte))
}
pub fn code_points(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
let mut offset = 0;
std::iter::from_fn(move || {
let unit = *self.0.get(offset)?;
let current_offset = offset;
offset += 1;
if is_high_surrogate(unit)
&& let Some(&low) = self.0.get(offset)
&& is_low_surrogate(low)
{
offset += 1;
Some((
current_offset,
0x1_0000 + ((u32::from(unit) - 0xD800) << 10) + (u32::from(low) - 0xDC00),
))
} else {
Some((current_offset, u32::from(unit)))
}
})
}
pub fn to_utf8_strict(&self) -> Result<String, IllFormedUtf16> {
if let Some(unit_offset) = self.first_ill_formed_offset() {
return Err(IllFormedUtf16 { unit_offset });
}
String::from_utf16(&self.0).map_err(|_| unreachable!("UTF-16 was validated"))
}
#[must_use]
pub fn to_utf8_lossy(&self) -> String {
String::from_utf16_lossy(&self.0)
}
fn first_ill_formed_offset(&self) -> Option<usize> {
let mut offset = 0;
while let Some(&unit) = self.0.get(offset) {
if is_high_surrogate(unit) {
if self
.0
.get(offset + 1)
.is_some_and(|&next| is_low_surrogate(next))
{
offset += 2;
} else {
return Some(offset);
}
} else if is_low_surrogate(unit) {
return Some(offset);
} else {
offset += 1;
}
}
None
}
}
impl fmt::Debug for EcmaString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("EcmaString(\"")?;
for (_, code_point) in self.code_points() {
if let Some(character) = char::from_u32(code_point) {
for escaped in character.escape_debug() {
formatter.write_char(escaped)?;
}
} else {
write!(formatter, "\\u{code_point:04X}")?;
}
}
formatter.write_str("\")")
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct IllFormedUtf16 {
pub unit_offset: usize,
}
impl fmt::Display for IllFormedUtf16 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"ill-formed UTF-16: unpaired surrogate at code-unit offset {}",
self.unit_offset
)
}
}
impl Error for IllFormedUtf16 {}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct InvalidCodePoint {
pub value: u32,
}
impl fmt::Display for InvalidCodePoint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "invalid Unicode code point U+{:X}", self.value)
}
}
impl Error for InvalidCodePoint {}
#[derive(Default)]
pub struct EcmaStringBuilder(Vec<u16>);
impl EcmaStringBuilder {
#[must_use]
pub const fn new() -> Self {
Self(Vec::new())
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
pub fn push_unit(&mut self, unit: u16) {
self.0.push(unit);
}
pub fn push_utf8(&mut self, value: &str) {
self.0.extend(value.encode_utf16());
}
pub fn push_code_point(&mut self, code_point: u32) -> Result<(), InvalidCodePoint> {
if code_point > 0x10_FFFF {
return Err(InvalidCodePoint { value: code_point });
}
if code_point <= 0xFFFF {
self.0.push(code_point as u16);
} else {
let supplementary = code_point - 0x1_0000;
self.0.push(0xD800 | ((supplementary >> 10) as u16));
self.0.push(0xDC00 | ((supplementary as u16) & 0x03FF));
}
Ok(())
}
#[must_use]
pub fn len_units(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn finish(self) -> EcmaString {
EcmaString(Arc::from(self.0))
}
}
const fn is_high_surrogate(unit: u16) -> bool {
unit >= 0xD800 && unit <= 0xDBFF
}
const fn is_low_surrogate(unit: u16) -> bool {
unit >= 0xDC00 && unit <= 0xDFFF
}
#[cfg(test)]
mod tests {
use super::{EcmaString, EcmaStringBuilder};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[test]
fn little_endian_wire_units_are_exact() {
let string = EcmaString::from_le_bytes(&[0x61, 0, 0, 0xD8, 0, 0xDC]);
assert_eq!(string.as_units(), &[0x0061, 0xD800, 0xDC00]);
}
#[test]
fn lexical_order_is_by_code_units() {
let high = EcmaString::from_units(&[0xD800]);
let low = EcmaString::from_units(&[0xDFFF]);
let supplementary = EcmaString::from_units(&[0xD800, 0xDC00]);
assert!(high < low);
assert!(high < supplementary);
assert!(supplementary < low);
}
#[test]
fn cloned_strings_are_equal_and_hash_equally() {
let original = EcmaString::from_units(&[0x61, 0xD800]);
let clone = original.clone();
let mut original_hasher = DefaultHasher::new();
let mut clone_hasher = DefaultHasher::new();
original.hash(&mut original_hasher);
clone.hash(&mut clone_hasher);
assert_eq!(original, clone);
assert_eq!(original_hasher.finish(), clone_hasher.finish());
}
#[test]
fn strict_utf8_reports_the_first_unpaired_surrogate() {
let string = EcmaString::from_units(&[0xD800, 0x61, 0xDC00]);
assert_eq!(string.to_utf8_strict().unwrap_err().unit_offset, 0);
assert_eq!(
EcmaString::from_units(&[0x61, 0xDC00])
.to_utf8_strict()
.unwrap_err()
.unit_offset,
1
);
}
#[test]
fn lossy_utf8_replaces_only_unpaired_surrogates() {
let string = EcmaString::from_units(&[0xD800, 0xDC00, 0xD800, 0x61, 0xDC00]);
assert_eq!(string.to_utf8_lossy(), "𐀀�a�");
}
#[test]
fn code_points_keep_code_unit_offsets_and_raw_surrogates() {
let string = EcmaString::from_units(&[0x61, 0xD800, 0xDC00, 0xDC00, 0xD800]);
assert_eq!(
string.code_points().collect::<Vec<_>>(),
vec![(0, 0x61), (1, 0x1_0000), (3, 0xDC00), (4, 0xD800)]
);
}
#[test]
fn slices_may_split_surrogate_pairs() {
let string = EcmaString::from_units(&[0xD800, 0xDC00]);
assert_eq!(string.slice_units(0..1).as_units(), &[0xD800]);
assert_eq!(string.slice_units(1..2).as_units(), &[0xDC00]);
}
#[test]
fn builder_preserves_supplementary_and_surrogate_code_points() {
let mut builder = EcmaStringBuilder::new();
builder.push_code_point(0x1F600).unwrap();
builder.push_code_point(0xD800).unwrap();
assert_eq!(builder.len_units(), 3);
assert_eq!(builder.finish().as_units(), &[0xD83D, 0xDE00, 0xD800]);
}
#[test]
fn builder_rejects_out_of_range_code_points() {
let mut builder = EcmaStringBuilder::new();
assert_eq!(
builder.push_code_point(0x11_0000).unwrap_err().value,
0x11_0000
);
assert!(builder.finish().is_empty());
}
#[test]
fn ascii_comparison_rejects_non_ascii_values() {
let ascii = EcmaString::from_utf8("ascii");
let non_ascii = EcmaString::from_utf8("é");
assert!(ascii.eq_ascii("ascii"));
assert!(!ascii.eq_ascii("ASCII"));
assert!(!ascii.eq_ascii("é"));
assert!(!non_ascii.eq_ascii("é"));
}
#[test]
fn debug_renders_lone_surrogates_visibly() {
let string = EcmaString::from_units(&[0x61, 0xD800, 0xDC00, 0xDC00]);
assert_eq!(format!("{string:?}"), "EcmaString(\"a𐀀\\uDC00\")");
}
}