1use std::error::Error;
2use std::fmt;
3use std::fmt::Write as _;
4use std::ops::Range;
5use std::sync::Arc;
6
7#[derive(Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct EcmaString(Arc<[u16]>);
13
14impl EcmaString {
15 #[must_use]
17 pub fn from_utf8(value: &str) -> Self {
18 Self(Arc::from(value.encode_utf16().collect::<Vec<_>>()))
19 }
20
21 #[must_use]
23 pub fn from_units(units: &[u16]) -> Self {
24 Self(Arc::from(units))
25 }
26
27 #[must_use]
29 pub(crate) fn from_le_bytes(bytes: &[u8]) -> Self {
30 debug_assert!(bytes.len().is_multiple_of(2));
31 Self(
32 bytes
33 .chunks_exact(2)
34 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
35 .collect::<Arc<[u16]>>(),
36 )
37 }
38
39 #[must_use]
41 pub fn as_units(&self) -> &[u16] {
42 &self.0
43 }
44
45 #[must_use]
47 pub fn len_units(&self) -> usize {
48 self.0.len()
49 }
50
51 #[must_use]
53 pub fn is_empty(&self) -> bool {
54 self.0.is_empty()
55 }
56
57 #[must_use]
59 pub fn unit_at(&self, offset: usize) -> Option<u16> {
60 self.0.get(offset).copied()
61 }
62
63 #[must_use]
68 pub fn slice_units(&self, range: Range<usize>) -> Self {
69 Self::from_units(&self.0[range])
70 }
71
72 #[must_use]
74 pub fn is_well_formed(&self) -> bool {
75 self.first_ill_formed_offset().is_none()
76 }
77
78 #[must_use]
80 pub fn eq_ascii(&self, value: &str) -> bool {
81 value.is_ascii()
82 && self.0.len() == value.len()
83 && self
84 .0
85 .iter()
86 .zip(value.bytes())
87 .all(|(&unit, byte)| unit == u16::from(byte))
88 }
89
90 pub fn code_points(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
95 let mut offset = 0;
96 std::iter::from_fn(move || {
97 let unit = *self.0.get(offset)?;
98 let current_offset = offset;
99 offset += 1;
100 if is_high_surrogate(unit)
101 && let Some(&low) = self.0.get(offset)
102 && is_low_surrogate(low)
103 {
104 offset += 1;
105 Some((
106 current_offset,
107 0x1_0000 + ((u32::from(unit) - 0xD800) << 10) + (u32::from(low) - 0xDC00),
108 ))
109 } else {
110 Some((current_offset, u32::from(unit)))
111 }
112 })
113 }
114
115 pub fn to_utf8_strict(&self) -> Result<String, IllFormedUtf16> {
122 if let Some(unit_offset) = self.first_ill_formed_offset() {
123 return Err(IllFormedUtf16 { unit_offset });
124 }
125 String::from_utf16(&self.0).map_err(|_| unreachable!("UTF-16 was validated"))
126 }
127
128 #[must_use]
130 pub fn to_utf8_lossy(&self) -> String {
131 String::from_utf16_lossy(&self.0)
132 }
133
134 fn first_ill_formed_offset(&self) -> Option<usize> {
135 let mut offset = 0;
136 while let Some(&unit) = self.0.get(offset) {
137 if is_high_surrogate(unit) {
138 if self
139 .0
140 .get(offset + 1)
141 .is_some_and(|&next| is_low_surrogate(next))
142 {
143 offset += 2;
144 } else {
145 return Some(offset);
146 }
147 } else if is_low_surrogate(unit) {
148 return Some(offset);
149 } else {
150 offset += 1;
151 }
152 }
153 None
154 }
155}
156
157impl fmt::Debug for EcmaString {
158 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159 formatter.write_str("EcmaString(\"")?;
160 for (_, code_point) in self.code_points() {
161 if let Some(character) = char::from_u32(code_point) {
162 for escaped in character.escape_debug() {
163 formatter.write_char(escaped)?;
164 }
165 } else {
166 write!(formatter, "\\u{code_point:04X}")?;
167 }
168 }
169 formatter.write_str("\")")
170 }
171}
172
173#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
175pub struct IllFormedUtf16 {
176 pub unit_offset: usize,
178}
179
180impl fmt::Display for IllFormedUtf16 {
181 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(
183 formatter,
184 "ill-formed UTF-16: unpaired surrogate at code-unit offset {}",
185 self.unit_offset
186 )
187 }
188}
189
190impl Error for IllFormedUtf16 {}
191
192#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194pub struct InvalidCodePoint {
195 pub value: u32,
197}
198
199impl fmt::Display for InvalidCodePoint {
200 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(formatter, "invalid Unicode code point U+{:X}", self.value)
202 }
203}
204
205impl Error for InvalidCodePoint {}
206
207#[derive(Default)]
209pub struct EcmaStringBuilder(Vec<u16>);
210
211impl EcmaStringBuilder {
212 #[must_use]
214 pub const fn new() -> Self {
215 Self(Vec::new())
216 }
217
218 #[must_use]
220 pub fn with_capacity(capacity: usize) -> Self {
221 Self(Vec::with_capacity(capacity))
222 }
223
224 pub fn push_unit(&mut self, unit: u16) {
226 self.0.push(unit);
227 }
228
229 pub fn push_utf8(&mut self, value: &str) {
231 self.0.extend(value.encode_utf16());
232 }
233
234 pub fn push_code_point(&mut self, code_point: u32) -> Result<(), InvalidCodePoint> {
244 if code_point > 0x10_FFFF {
245 return Err(InvalidCodePoint { value: code_point });
246 }
247 if code_point <= 0xFFFF {
248 self.0.push(code_point as u16);
249 } else {
250 let supplementary = code_point - 0x1_0000;
251 self.0.push(0xD800 | ((supplementary >> 10) as u16));
252 self.0.push(0xDC00 | ((supplementary as u16) & 0x03FF));
253 }
254 Ok(())
255 }
256
257 #[must_use]
259 pub fn len_units(&self) -> usize {
260 self.0.len()
261 }
262
263 #[must_use]
265 pub fn finish(self) -> EcmaString {
266 EcmaString(Arc::from(self.0))
267 }
268}
269
270const fn is_high_surrogate(unit: u16) -> bool {
271 unit >= 0xD800 && unit <= 0xDBFF
272}
273
274const fn is_low_surrogate(unit: u16) -> bool {
275 unit >= 0xDC00 && unit <= 0xDFFF
276}
277
278#[cfg(test)]
279mod tests {
280 use super::{EcmaString, EcmaStringBuilder};
281 use std::collections::hash_map::DefaultHasher;
282 use std::hash::{Hash, Hasher};
283
284 #[test]
285 fn little_endian_wire_units_are_exact() {
286 let string = EcmaString::from_le_bytes(&[0x61, 0, 0, 0xD8, 0, 0xDC]);
287 assert_eq!(string.as_units(), &[0x0061, 0xD800, 0xDC00]);
288 }
289
290 #[test]
291 fn lexical_order_is_by_code_units() {
292 let high = EcmaString::from_units(&[0xD800]);
293 let low = EcmaString::from_units(&[0xDFFF]);
294 let supplementary = EcmaString::from_units(&[0xD800, 0xDC00]);
295
296 assert!(high < low);
297 assert!(high < supplementary);
298 assert!(supplementary < low);
299 }
300
301 #[test]
302 fn cloned_strings_are_equal_and_hash_equally() {
303 let original = EcmaString::from_units(&[0x61, 0xD800]);
304 let clone = original.clone();
305 let mut original_hasher = DefaultHasher::new();
306 let mut clone_hasher = DefaultHasher::new();
307 original.hash(&mut original_hasher);
308 clone.hash(&mut clone_hasher);
309
310 assert_eq!(original, clone);
311 assert_eq!(original_hasher.finish(), clone_hasher.finish());
312 }
313
314 #[test]
315 fn strict_utf8_reports_the_first_unpaired_surrogate() {
316 let string = EcmaString::from_units(&[0xD800, 0x61, 0xDC00]);
317
318 assert_eq!(string.to_utf8_strict().unwrap_err().unit_offset, 0);
319 assert_eq!(
320 EcmaString::from_units(&[0x61, 0xDC00])
321 .to_utf8_strict()
322 .unwrap_err()
323 .unit_offset,
324 1
325 );
326 }
327
328 #[test]
329 fn lossy_utf8_replaces_only_unpaired_surrogates() {
330 let string = EcmaString::from_units(&[0xD800, 0xDC00, 0xD800, 0x61, 0xDC00]);
331
332 assert_eq!(string.to_utf8_lossy(), "𐀀�a�");
333 }
334
335 #[test]
336 fn code_points_keep_code_unit_offsets_and_raw_surrogates() {
337 let string = EcmaString::from_units(&[0x61, 0xD800, 0xDC00, 0xDC00, 0xD800]);
338
339 assert_eq!(
340 string.code_points().collect::<Vec<_>>(),
341 vec![(0, 0x61), (1, 0x1_0000), (3, 0xDC00), (4, 0xD800)]
342 );
343 }
344
345 #[test]
346 fn slices_may_split_surrogate_pairs() {
347 let string = EcmaString::from_units(&[0xD800, 0xDC00]);
348
349 assert_eq!(string.slice_units(0..1).as_units(), &[0xD800]);
350 assert_eq!(string.slice_units(1..2).as_units(), &[0xDC00]);
351 }
352
353 #[test]
354 fn builder_preserves_supplementary_and_surrogate_code_points() {
355 let mut builder = EcmaStringBuilder::new();
356 builder.push_code_point(0x1F600).unwrap();
357 builder.push_code_point(0xD800).unwrap();
358
359 assert_eq!(builder.len_units(), 3);
360 assert_eq!(builder.finish().as_units(), &[0xD83D, 0xDE00, 0xD800]);
361 }
362
363 #[test]
364 fn builder_rejects_out_of_range_code_points() {
365 let mut builder = EcmaStringBuilder::new();
366
367 assert_eq!(
368 builder.push_code_point(0x11_0000).unwrap_err().value,
369 0x11_0000
370 );
371 assert!(builder.finish().is_empty());
372 }
373
374 #[test]
375 fn ascii_comparison_rejects_non_ascii_values() {
376 let ascii = EcmaString::from_utf8("ascii");
377 let non_ascii = EcmaString::from_utf8("é");
378
379 assert!(ascii.eq_ascii("ascii"));
380 assert!(!ascii.eq_ascii("ASCII"));
381 assert!(!ascii.eq_ascii("é"));
382 assert!(!non_ascii.eq_ascii("é"));
383 }
384
385 #[test]
386 fn debug_renders_lone_surrogates_visibly() {
387 let string = EcmaString::from_units(&[0x61, 0xD800, 0xDC00, 0xDC00]);
388
389 assert_eq!(format!("{string:?}"), "EcmaString(\"a𐀀\\uDC00\")");
390 }
391}