Skip to main content

xutf/
convert.rs

1//! Transcoding between encodings (`utf_convert` / `utf_length`).
2
3use alloc::{
4	string::{FromUtf8Error, String},
5	vec,
6	vec::Vec,
7};
8
9use crate::{
10	encoding::{Encoding, Kind, same_encoding},
11	simd::{NARROW, TRANS_WIDE, ascii_run},
12	unit::{Unit, fold_case_scalar},
13	utf8::Utf8,
14};
15
16/// Owned destination of [`Text::transcode`](crate::Text::transcode); pins the
17/// target encoding through its code-unit type.
18///
19/// Implemented for [`String`] and `Vec<u8>` (UTF-8), `Vec<u16>` (UTF-16) and
20/// `Vec<u32>` (UTF-32), all native-endian.
21pub trait TextBuf: Sized {
22	/// Code-unit type of this buffer; selects [`Unit::Native`] as the encoding
23	/// transcoded into.
24	type Unit: Unit;
25
26	/// Wraps freshly transcoded units.
27	///
28	/// Units come from a permissive transcode, so [`String`] substitutes
29	/// U+FFFD for anything that is not valid UTF-8.
30	fn from_units(units: Vec<Self::Unit>) -> Self;
31}
32
33impl TextBuf for String {
34	type Unit = u8;
35
36	#[inline]
37	fn from_units(units: Vec<u8>) -> Self {
38		match Self::from_utf8(units) {
39			Ok(s) => s,
40			Err(e) => Self::from_utf8_lossy(&e.into_bytes()).into_owned(),
41		}
42	}
43}
44
45impl TextBuf for Vec<u8> {
46	type Unit = u8;
47
48	#[inline]
49	fn from_units(units: Self) -> Self {
50		units
51	}
52}
53
54impl TextBuf for Vec<u16> {
55	type Unit = u16;
56
57	#[inline]
58	fn from_units(units: Self) -> Self {
59		units
60	}
61}
62
63impl TextBuf for Vec<u32> {
64	type Unit = u32;
65
66	#[inline]
67	fn from_units(units: Self) -> Self {
68		units
69	}
70}
71
72/// ASCII case transform applied while transcoding.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum AsciiCase {
75	/// Copy as-is.
76	Preserve,
77	/// Fold `A-Z` to `a-z`.
78	Lower,
79	/// Fold `a-z` to `A-Z`.
80	Upper,
81}
82
83impl AsciiCase {
84	/// Fold base: subtracting it maps the source range onto `0..=25`.
85	#[inline(always)]
86	const fn base(self) -> u32 {
87		match self {
88			// Never matches when folding is disabled (0x00..=0x19 are not
89			// reachable after the ASCII-alpha guard below), but Preserve is
90			// filtered out before use anyway.
91			Self::Preserve => u32::MAX,
92			Self::Lower => 'A' as u32,
93			Self::Upper => 'a' as u32,
94		}
95	}
96}
97
98/// Cross-width unit bridge; a no-op at machine level for equal widths.
99#[inline(always)]
100fn bridge<F: Unit, T: Unit>(u: F) -> T {
101	T::from_u32(u.to_u32())
102}
103
104/// Transcodes as much of `src` into `dst` as fits, returning
105/// `(units_read, units_written)`. Never splits a codepoint: encoding stops
106/// at the last codepoint whose output fits.
107///
108/// `dst` past the written count may be clobbered with scratch by the SIMD
109/// fast path.
110///
111/// When `F` and `T` are the same encoding and `case` is `Preserve`, this
112/// degrades to a bulk copy of `min(src.len(), dst.len())` units and *can*
113/// split a trailing codepoint, mirroring the original's memcpy shortcut.
114#[inline]
115pub fn transcode_into<F: Encoding, T: Encoding>(
116	src: &[F::Unit],
117	dst: &mut [T::Unit],
118	case: AsciiCase,
119) -> (usize, usize) {
120	let same = const { same_encoding::<F, T>() };
121	if same && case == AsciiCase::Preserve {
122		let n = src.len().min(dst.len());
123		for (d, s) in dst[..n].iter_mut().zip(&src[..n]) {
124			*d = bridge(*s);
125		}
126		return (n, n);
127	}
128
129	if case == AsciiCase::Preserve
130		&& let Some(result) = crate::native::transcode::<F, T>(src, dst)
131	{
132		return result;
133	}
134
135	// The SIMD path stores raw lane values; with byte-swapped units ASCII is
136	// not lane-ASCII, so foreign encodings on either side go scalar.
137	let simd_ok = const { !F::FOREIGN && !T::FOREIGN };
138	let fold = case != AsciiCase::Preserve;
139	let base = case.base();
140
141	let mut i = 0;
142	let mut o = 0;
143	while i < src.len() {
144		if simd_ok {
145			let limit = (src.len() - i).min(dst.len() - o);
146			let n = if limit >= TRANS_WIDE {
147				ascii_run::<F::Unit, TRANS_WIDE>(&src[i..], limit, |v, at| {
148					let mut w = F::Unit::cast::<T::Unit, TRANS_WIDE>(v);
149					if fold {
150						w = T::Unit::fold_case(w, base);
151					}
152					w.copy_to_slice(&mut dst[o + at..o + at + TRANS_WIDE]);
153				})
154			} else if limit >= NARROW {
155				ascii_run::<F::Unit, NARROW>(&src[i..], limit, |v, at| {
156					let mut w = F::Unit::cast::<T::Unit, NARROW>(v);
157					if fold {
158						w = T::Unit::fold_case(w, base);
159					}
160					w.copy_to_slice(&mut dst[o + at..o + at + NARROW]);
161				})
162			} else {
163				0
164			};
165			i += n;
166			o += n;
167			if i == src.len() {
168				break;
169			}
170		}
171
172		if same {
173			// Same encoding: copy the encoded run verbatim, folding only the
174			// lead unit (ASCII alphas are always single-unit runs).
175			let front = src[i];
176			// A truncated tail is still one permissive codepoint; copy the
177			// available run instead of leaving it unread.
178			let len = F::run_length(front).min(src.len() - i);
179			if dst.len() - o < len {
180				break;
181			}
182			let mut lead = front.to_u32();
183			if fold {
184				let native = if F::FOREIGN {
185					front.swap_bytes().to_u32()
186				} else {
187					lead
188				};
189				if native.wrapping_sub(base) <= 25 {
190					let folded = F::Unit::from_u32(native ^ 0x20);
191					lead = if F::FOREIGN {
192						folded.swap_bytes().to_u32()
193					} else {
194						folded.to_u32()
195					};
196				}
197			}
198			dst[o] = T::Unit::from_u32(lead);
199			for k in 1..len {
200				dst[o + k] = bridge(src[i + k]);
201			}
202			i += len;
203			o += len;
204		} else {
205			let mut cur = &src[i..];
206			let mut cp = F::decode(&mut cur);
207			let consumed = src.len() - i - cur.len();
208			if fold {
209				cp = fold_case_scalar(cp, base);
210			}
211			if dst.len() - o < T::encoded_length(cp) {
212				break; // Unlike the C++, the unencoded codepoint stays unread.
213			}
214			o += T::encode(cp, &mut dst[o..]);
215			i += consumed;
216		}
217	}
218	(i, o)
219}
220
221/// Transcodes all of `src` into a freshly allocated unit vector.
222#[inline]
223pub fn transcode<F: Encoding, T: Encoding>(src: &[F::Unit]) -> Vec<T::Unit> {
224	transcode_with_case::<F, T>(src, AsciiCase::Preserve)
225}
226
227/// [`transcode`] with ASCII case folding.
228pub fn transcode_with_case<F: Encoding, T: Encoding>(
229	src: &[F::Unit],
230	case: AsciiCase,
231) -> Vec<T::Unit> {
232	let mut out = vec![T::Unit::default(); src.len() * T::MAX_UNITS];
233	let (read, written) = transcode_into::<F, T>(src, &mut out, case);
234	debug_assert_eq!(read, src.len());
235	out.truncate(written);
236	out
237}
238
239/// Unit count `src` would occupy once transcoded to `T` (`utf_length`).
240pub fn transcoded_len<F: Encoding, T: Encoding>(src: &[F::Unit]) -> usize {
241	// One unit in, one unit out — except UTF-16 across byte orders, where a
242	// high surrogate followed by a non-surrogate decodes from two units and
243	// re-encodes as one (byte-identical encodings take the bulk copy in
244	// `transcode_into` and keep every unit).
245	let unit_preserving = const {
246		size_of::<F::Unit>() == size_of::<T::Unit>()
247			&& (same_encoding::<F, T>() || F::KIND as u8 != Kind::Utf16 as u8)
248	};
249	if unit_preserving {
250		return src.len();
251	}
252	let simd_ok = const { !F::FOREIGN };
253	let mut n = 0;
254	let mut i = 0;
255	while i < src.len() {
256		if simd_ok {
257			let limit = src.len() - i;
258			let c = if limit >= TRANS_WIDE {
259				ascii_run::<F::Unit, TRANS_WIDE>(&src[i..], limit, |_, _| {})
260			} else if limit >= NARROW {
261				ascii_run::<F::Unit, NARROW>(&src[i..], limit, |_, _| {})
262			} else {
263				0
264			};
265			i += c;
266			n += c;
267			if i == src.len() {
268				break;
269			}
270		}
271		let mut cur = &src[i..];
272		let cp = F::decode(&mut cur);
273		i = src.len() - cur.len();
274		n += T::encoded_length(cp);
275	}
276	n
277}
278
279/// Transcodes to an owned `String`.
280///
281/// # Errors
282/// When the source decodes to invalid scalar values (e.g. lone surrogates),
283/// which would make the resulting UTF-8 invalid.
284pub fn to_string<F: Encoding>(src: &[F::Unit]) -> Result<String, FromUtf8Error> {
285	String::from_utf8(transcode::<F, Utf8>(src))
286}