Skip to main content

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