Skip to main content

css_parse/
arena_string.rs

1use crate::{Arena, Vec};
2use allocator_api2::alloc::Allocator;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::io;
6use std::ops::Deref;
7
8/// A growable, arena-allocated UTF-8 string.
9///
10/// Wraps [`Vec<u8>`][crate::Vec] the same way [`std::string::String`] wraps [`std::vec::Vec`], so it inherits the
11/// allocator generic from [Vec] too. Like the other arena collections it never runs destructors: the bytes are
12/// released wholesale when the arena is dropped.
13///
14/// The contents are always valid UTF-8: the only ways to append are [`String::push_str`], [`String::push`] and the
15/// [`fmt::Write`] impl, all of which take a `str` or a `char`, and nothing exposes the bytes mutably or truncates
16/// them. [`String::from_reader_in`] is the sole byte-wise entry point and validates before handing back a `String`.
17/// [`String::as_str`] and [`String::into_str`] rely on that invariant to hand out a `str` without revalidating.
18#[repr(C)]
19pub struct String<'a, A: Allocator = &'a Arena> {
20	bytes: Vec<'a, u8, A>,
21}
22
23impl<'a, A: Allocator> String<'a, A> {
24	/// Create a new, empty `String` backed by `alloc`. Allocates nothing until the first push.
25	#[inline]
26	pub fn new_in(alloc: A) -> Self {
27		Self { bytes: Vec::new_in(alloc) }
28	}
29
30	/// Create an empty `String` with room for at least `cap` bytes.
31	#[inline]
32	pub fn with_capacity_in(cap: usize, alloc: A) -> Self {
33		Self { bytes: Vec::with_capacity_in(cap, alloc) }
34	}
35
36	/// Read `reader` to end into a new `String`, the arena equivalent of [`std::io::Read::read_to_string`].
37	///
38	/// The bytes are validated as UTF-8 once, on the whole buffer, before the `String` exists; an invalid stream is
39	/// reported as [`std::io::ErrorKind::InvalidData`] and no `String` is returned.
40	pub fn from_reader_in<R: io::Read>(mut reader: R, alloc: A) -> io::Result<Self> {
41		/// Bytes offered to each `read` call; the arena `Vec` doubles its capacity as this is appended.
42		const CHUNK: usize = 8 * 1024;
43		let mut bytes = Vec::new_in(alloc);
44		let mut filled = 0;
45		loop {
46			if filled == bytes.len() {
47				bytes.extend_from_slice(&[0; CHUNK]);
48			}
49			match reader.read(&mut bytes[filled..]) {
50				Ok(0) => break,
51				Ok(read) => filled += read,
52				Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
53				Err(err) => return Err(err),
54			}
55		}
56		bytes.truncate(filled);
57		match std::str::from_utf8(&bytes) {
58			Ok(_) => Ok(Self { bytes }),
59			Err(err) => Err(io::Error::new(io::ErrorKind::InvalidData, err)),
60		}
61	}
62
63	/// Length in bytes, not characters.
64	#[inline]
65	pub fn len(&self) -> usize {
66		self.bytes.len()
67	}
68
69	#[inline]
70	pub fn is_empty(&self) -> bool {
71		self.bytes.is_empty()
72	}
73
74	/// Capacity in bytes.
75	#[inline]
76	pub fn capacity(&self) -> usize {
77		self.bytes.capacity()
78	}
79
80	/// Append a string slice.
81	#[inline]
82	pub fn push_str(&mut self, str: &str) {
83		self.bytes.extend_from_slice(str.as_bytes());
84	}
85
86	/// Append a single character, encoded as UTF-8.
87	#[inline]
88	pub fn push(&mut self, char: char) {
89		let mut buf = [0; 4];
90		self.push_str(char.encode_utf8(&mut buf));
91	}
92
93	/// Borrow the contents as a `str`.
94	#[inline]
95	pub fn as_str(&self) -> &str {
96		Self::as_utf8(&self.bytes)
97	}
98
99	/// Consume the `String`, returning a `str` borrowed from the arena for `'a`.
100	///
101	/// Use this to hand a parsed-in-arena string to an API that wants `&'a str`; the bytes outlive the `String` because
102	/// they belong to the arena, not to this handle.
103	#[inline]
104	pub fn into_str(self) -> &'a str {
105		Self::as_utf8(self.bytes.into_slice())
106	}
107
108	#[inline]
109	fn as_utf8(bytes: &[u8]) -> &str {
110		debug_assert!(std::str::from_utf8(bytes).is_ok(), "arena String must always hold valid UTF-8");
111		// SAFETY: the buffer only ever grows through `push_str`, `push` and the `fmt::Write` impl, each of which appends a
112		// `str` or a UTF-8 encoded `char`, or through `from_reader_in`, which validates the whole buffer before
113		// constructing the `String`. Nothing hands the bytes out mutably or truncates them.
114		unsafe { std::str::from_utf8_unchecked(bytes) }
115	}
116}
117
118impl<'a, A: Allocator> Deref for String<'a, A> {
119	type Target = str;
120
121	#[inline]
122	fn deref(&self) -> &str {
123		self.as_str()
124	}
125}
126
127impl<'a, A: Allocator> AsRef<str> for String<'a, A> {
128	#[inline]
129	fn as_ref(&self) -> &str {
130		self.as_str()
131	}
132}
133
134impl<'a, A: Allocator> fmt::Write for String<'a, A> {
135	#[inline]
136	fn write_str(&mut self, str: &str) -> fmt::Result {
137		self.push_str(str);
138		Ok(())
139	}
140}
141
142impl<'a, A: Allocator> fmt::Display for String<'a, A> {
143	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144		fmt::Display::fmt(self.as_str(), f)
145	}
146}
147
148impl<'a, A: Allocator> fmt::Debug for String<'a, A> {
149	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150		fmt::Debug::fmt(self.as_str(), f)
151	}
152}
153
154impl<'a, A: Allocator> PartialEq for String<'a, A> {
155	fn eq(&self, other: &Self) -> bool {
156		**self == **other
157	}
158}
159
160impl<'a, A: Allocator> Eq for String<'a, A> {}
161
162impl<'a, A: Allocator> PartialEq<str> for String<'a, A> {
163	fn eq(&self, other: &str) -> bool {
164		&**self == other
165	}
166}
167
168impl<'a, A: Allocator> PartialEq<&str> for String<'a, A> {
169	fn eq(&self, other: &&str) -> bool {
170		&**self == *other
171	}
172}
173
174impl<'a, A: Allocator> Hash for String<'a, A> {
175	fn hash<H: Hasher>(&self, state: &mut H) {
176		(**self).hash(state);
177	}
178}
179
180/// A [`std::format!`]-style constructor for the arena [`String`].
181///
182/// ```
183/// use css_parse::{Arena, format_in};
184/// let alloc = Arena::default();
185/// let str = format_in!(in &alloc, "{}px", 12);
186/// assert_eq!(str.as_str(), "12px");
187/// ```
188#[macro_export]
189macro_rules! format_in {
190	(in $alloc:expr, $($arg:tt)*) => {{
191		let mut str = $crate::String::new_in($alloc);
192		::core::fmt::Write::write_fmt(&mut str, ::core::format_args!($($arg)*))
193			.expect("formatting into an arena String cannot fail");
194		str
195	}};
196}
197
198#[cfg(test)]
199mod test {
200	use super::String;
201	use crate::Arena;
202	use std::fmt::Write;
203	use std::io::{self, Read};
204
205	/// Hands out one byte per `read`, with a single `Interrupted` in the middle, as a real socket may.
206	struct DribbleReader<'r> {
207		bytes: &'r [u8],
208		interrupt_at: usize,
209		reads: usize,
210	}
211
212	impl<'r> Read for DribbleReader<'r> {
213		fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
214			self.reads += 1;
215			if self.reads == self.interrupt_at {
216				return Err(io::Error::from(io::ErrorKind::Interrupted));
217			}
218			let Some((first, rest)) = self.bytes.split_first() else {
219				return Ok(0);
220			};
221			buf[0] = *first;
222			self.bytes = rest;
223			Ok(1)
224		}
225	}
226
227	#[test]
228	fn new_is_empty_and_allocates_nothing() {
229		let alloc = Arena::default();
230		let str = String::new_in(&alloc);
231		assert!(str.is_empty());
232		assert_eq!(str.len(), 0);
233		assert_eq!(str.as_str(), "");
234	}
235
236	#[test]
237	fn push_str_appends_in_order() {
238		let alloc = Arena::default();
239		let mut str = String::new_in(&alloc);
240		str.push_str("foo");
241		str.push_str("bar");
242		assert_eq!(str.as_str(), "foobar");
243		assert_eq!(str.len(), 6);
244	}
245
246	#[test]
247	fn push_encodes_multibyte_chars() {
248		let alloc = Arena::default();
249		let mut str = String::new_in(&alloc);
250		str.push('a');
251		str.push('£');
252		str.push('😀');
253		assert_eq!(str.as_str(), "a£😀");
254		// 1 + 2 + 4 bytes, so length counts bytes rather than characters.
255		assert_eq!(str.len(), 7);
256	}
257
258	#[test]
259	fn write_macro_formats_through_fmt_write() {
260		let alloc = Arena::default();
261		let mut str = String::new_in(&alloc);
262		write!(&mut str, "{}px", 12).unwrap();
263		assert_eq!(str.as_str(), "12px");
264	}
265
266	#[test]
267	fn format_in_builds_a_string() {
268		let alloc = Arena::default();
269		let str = format_in!(in &alloc, "{}{}", 12, "px");
270		assert_eq!(str, "12px");
271	}
272
273	#[test]
274	fn into_str_outlives_the_string() {
275		let alloc = Arena::default();
276		let borrowed: &str = {
277			let mut str = String::new_in(&alloc);
278			str.push_str("outlives");
279			str.into_str()
280		};
281		assert_eq!(borrowed, "outlives");
282	}
283
284	#[test]
285	fn survives_reallocation() {
286		let alloc = Arena::default();
287		let mut str = String::new_in(&alloc);
288		for i in 0..512 {
289			write!(&mut str, "{}", i % 10).unwrap();
290		}
291		// Every byte must survive the regrowth, so compare against the whole expected sequence.
292		let expected: std::string::String = (0..512).map(|i| char::from(b'0' + (i % 10) as u8)).collect();
293		assert_eq!(str.len(), 512);
294		assert_eq!(str.as_str(), expected);
295	}
296
297	#[test]
298	fn with_capacity_reserves_without_writing() {
299		let alloc = Arena::default();
300		let mut str = String::with_capacity_in(16, &alloc);
301		assert!(str.is_empty());
302		assert!(str.capacity() >= 16);
303		str.push_str("fits");
304		assert_eq!(str.as_str(), "fits");
305	}
306
307	#[test]
308	fn deref_exposes_str_methods() {
309		let alloc = Arena::default();
310		let mut str = String::new_in(&alloc);
311		str.push_str("  padded  ");
312		assert_eq!(str.trim(), "padded");
313		assert!(str.contains("padded"));
314	}
315
316	#[test]
317	fn equality_against_str_and_self() {
318		let alloc = Arena::default();
319		let mut a = String::new_in(&alloc);
320		a.push_str("same");
321		let mut b = String::new_in(&alloc);
322		b.push_str("same");
323		assert_eq!(a, b);
324		assert_eq!(a, "same");
325		assert_eq!(a, *"same");
326	}
327
328	#[test]
329	fn from_reader_in_reads_to_end() {
330		let alloc = Arena::default();
331		let str = String::from_reader_in("body{color:blue}".as_bytes(), &alloc).unwrap();
332		assert_eq!(str.as_str(), "body{color:blue}");
333		assert_eq!(str.len(), 16);
334	}
335
336	#[test]
337	fn from_reader_in_rejects_invalid_utf8() {
338		let alloc = Arena::default();
339		let err = String::from_reader_in(&[b'a', 0xff, b'b'][..], &alloc).unwrap_err();
340		assert_eq!(err.kind(), io::ErrorKind::InvalidData);
341	}
342
343	#[test]
344	fn from_reader_in_grows_past_one_chunk() {
345		let alloc = Arena::default();
346		// A multibyte codepoint straddles the 8KiB read boundary, so a chunk-local decode would split it.
347		let mut expected = "x".repeat(8 * 1024 - 2);
348		expected.push('😀');
349		expected.push_str(&"y".repeat(4096));
350		let str = String::from_reader_in(expected.as_bytes(), &alloc).unwrap();
351		assert_eq!(str.len(), expected.len());
352		assert_eq!(str.as_str(), expected);
353	}
354
355	#[test]
356	fn from_reader_in_handles_partial_reads_and_interruptions() {
357		let alloc = Arena::default();
358		let reader = DribbleReader { bytes: "a£😀b".as_bytes(), interrupt_at: 3, reads: 0 };
359		let str = String::from_reader_in(reader, &alloc).unwrap();
360		assert_eq!(str.as_str(), "a£😀b");
361	}
362}