Skip to main content

surrealdb_strand/
lib.rs

1//! A small-string-optimised immutable string type used throughout the value layer.
2//!
3//! [`Strand`] backs string-shaped keys and values — `Value::String`, `Object` keys, `TableName`,
4//! `RecordIdKey::String` — and trades the mutability of `String` for three complementary storage
5//! strategies selected at construction time:
6//!
7//! * `Inline` — short strings (≤ [`INLINE_CAP`] bytes) stored directly on the stack, with no heap
8//!   allocation.
9//! * `Static` — a `&'static str` wrapper for compile-time known values. Construction is `const`,
10//!   clone is a pointer copy, and drop is a no-op. Ideal for long literals (table names, reserved
11//!   keywords, response keys) that would otherwise allocate. Build one with [`Strand::new_static`].
12//! * `Boxed` — dynamic long strings held in a `Box<str>`: one allocation per string, no atomic ops
13//!   on construction or drop. Clone does `malloc + memcpy`, so reach for [`Strand::new_static`]
14//!   whenever the value is known at compile time.
15//!
16//! ## Layout
17//!
18//! `Strand` uses a custom 24-byte union layout. The first 16 bytes (on 64-bit) always form a
19//! valid `&str` fat pointer for `Static` and `Boxed` strings. For `Inline` strings, the string
20//! data is stored inline. The 24th byte (index 23) is used as a tag:
21//! * `0..=23`: The string is `Inline`, and the tag is the length.
22//! * `254`: The string is `Static`.
23//! * `255`: The string is `Boxed`.
24//!
25//! This layout allows `as_str()` to be completely branchless, significantly improving the
26//! performance of equality and ordering comparisons for all variants.
27//!
28//! <section class="warning">
29//! <h3>Unstable!</h3>
30//! This crate is <b>SurrealDB internal API</b>. It does not adhere to SemVer and its API is
31//! free to change and break code even between patch versions. If you are looking for a stable
32//! interface to the SurrealDB library please have a look at
33//! <a href="https://crates.io/crates/surrealdb">the Rust SDK</a>.
34//! </section>
35
36use std::borrow::Borrow;
37use std::cmp::Ordering;
38use std::fmt::{Debug, Display};
39use std::hash::{Hash, Hasher};
40use std::mem::ManuallyDrop;
41use std::ops::Deref;
42
43use revision::{DeserializeRevisioned, Error, Revisioned, SerializeRevisioned};
44use serde::de::{self, Visitor};
45use serde::{Deserialize, Deserializer, Serialize, Serializer};
46
47/// Maximum byte length of a string that can be stored inline.
48pub const INLINE_CAP: usize = 23;
49
50/// Tag for static strings.
51const TAG_STATIC: u8 = 254;
52/// Tag for boxed strings.
53const TAG_BOXED: u8 = 255;
54
55/// Length of the padding bytes in the heap data.
56const HEAP_PAD_LEN: usize = 23 - 2 * std::mem::size_of::<usize>();
57
58/// Heap data for boxed strings.
59#[derive(Clone, Copy)]
60#[repr(C)]
61struct HeapData {
62	ptr: *const u8,
63	len: usize,
64	_pad: [u8; HEAP_PAD_LEN],
65	tag: u8,
66}
67
68/// Union for inline and heap data.
69#[repr(C)]
70union StrandData {
71	inline: [u8; 24],
72	heap: ManuallyDrop<HeapData>,
73}
74
75/// Immutable string with inline small-string optimisation.
76///
77/// See the [module docs](self) for the design rationale.
78#[repr(transparent)]
79pub struct Strand {
80	data: StrandData,
81}
82
83unsafe impl Send for Strand {}
84unsafe impl Sync for Strand {}
85
86impl Strand {
87	/// Create a new [`Strand`] from any string-like input.
88	#[inline]
89	pub fn new(s: impl AsRef<str>) -> Self {
90		let s = s.as_ref();
91		if s.len() <= INLINE_CAP {
92			Self::new_inline(s)
93		} else {
94			Self::from(Box::from(s))
95		}
96	}
97
98	#[inline]
99	fn new_inline(s: &str) -> Self {
100		debug_assert!(s.len() <= INLINE_CAP);
101		let mut inline = [0u8; 24];
102		// SAFETY: We checked the length above.
103		unsafe {
104			std::ptr::copy_nonoverlapping(s.as_ptr(), inline.as_mut_ptr(), s.len());
105		}
106		inline[23] = s.len() as u8;
107		Self {
108			data: StrandData {
109				inline,
110			},
111		}
112	}
113
114	/// Wrap a `&'static str` as a [`Strand`] without allocating.
115	///
116	/// This never allocates or copies; the returned `Strand` holds
117	/// the caller's fat pointer directly, and cloning it is a
118	/// bitwise copy. Callable in `const` context, so compile-time
119	/// `Strand` constants of arbitrary length are fine:
120	///
121	/// ```
122	/// # use surrealdb_strand::Strand;
123	/// const KIND: Strand = Strand::new_static("geometry<multipolygon>");
124	/// ```
125	#[inline(always)]
126	pub const fn new_static(text: &'static str) -> Self {
127		Self {
128			data: StrandData {
129				heap: ManuallyDrop::new(HeapData {
130					ptr: text.as_ptr(),
131					len: text.len(),
132					_pad: [0; HEAP_PAD_LEN],
133					tag: TAG_STATIC,
134				}),
135			},
136		}
137	}
138
139	/// Format a value directly into an inline `Strand`, bypassing any heap allocation.
140	///
141	/// If the formatted string exceeds `INLINE_CAP` bytes, this falls back to a heap-allocated
142	/// `Boxed` string. This is ideal for constructing short, dynamic strings (like keys or
143	/// identifiers) where the length is known or highly likely to be small.
144	pub fn from_display(d: impl Display) -> Self {
145		use std::fmt::Write;
146		// Custom writer for the inline buffer
147		struct StrandWriter {
148			inline: [u8; 24],
149			len: usize,
150			overflow: Option<String>,
151		}
152		// Implement the inline buffer writer
153		impl Write for StrandWriter {
154			fn write_str(&mut self, s: &str) -> std::fmt::Result {
155				// Check if we have overflowed already
156				if let Some(overflow) = &mut self.overflow {
157					overflow.push_str(s);
158					return Ok(());
159				}
160				// Get the string bytes
161				let bytes = s.as_bytes();
162				// Calculate the end length
163				let end = self.len + bytes.len();
164				// Check if it fits in the inline buffer
165				if end <= INLINE_CAP {
166					// It fits in the inline buffer
167					unsafe {
168						std::ptr::copy_nonoverlapping(
169							bytes.as_ptr(),
170							self.inline.as_mut_ptr().add(self.len),
171							bytes.len(),
172						);
173					}
174					self.len = end;
175				} else {
176					// It overflows! Convert what we have so far into a String, then append the new
177					// string.
178					let valid_utf8 =
179						unsafe { std::str::from_utf8_unchecked(&self.inline[..self.len]) };
180					let mut overflow = String::with_capacity(end);
181					overflow.push_str(valid_utf8);
182					overflow.push_str(s);
183					self.overflow = Some(overflow);
184				}
185				Ok(())
186			}
187		}
188		// Create a new writer
189		let mut writer = StrandWriter {
190			inline: [0u8; 24],
191			len: 0,
192			overflow: None,
193		};
194		// Write the displayable value into our custom writer
195		write!(&mut writer, "{}", d).expect("writing to StrandWriter should never fail");
196		// Check if we have an overflow
197		if let Some(overflow) = writer.overflow {
198			// It was too long, return a Boxed strand
199			Self::from(overflow)
200		} else {
201			// It fit perfectly! Set the tag/length and return the inline strand
202			writer.inline[23] = writer.len as u8;
203			Self {
204				data: StrandData {
205					inline: writer.inline,
206				},
207			}
208		}
209	}
210
211	/// Whether this string is stored inline (no heap allocation).
212	#[inline]
213	pub fn is_inline(&self) -> bool {
214		unsafe { self.data.inline[23] <= INLINE_CAP as u8 }
215	}
216
217	/// Whether this string wraps a `&'static str` (no allocation).
218	#[inline]
219	pub fn is_static(&self) -> bool {
220		unsafe { self.data.inline[23] == TAG_STATIC }
221	}
222
223	/// Whether this string is heap-allocated in a `Box<str>`.
224	#[inline]
225	pub fn is_boxed(&self) -> bool {
226		unsafe { self.data.inline[23] == TAG_BOXED }
227	}
228
229	/// Access the underlying string slice.
230	#[inline(always)]
231	pub fn as_str(&self) -> &str {
232		// SAFETY: The tag byte is strictly controlled during construction.
233		// It is either the length of an inline string (0..=23), TAG_STATIC (254),
234		// or TAG_BOXED (255). This allows for LLVM optimizations.
235		unsafe {
236			// Get the tag byte.
237			let tag = self.data.inline[23];
238			// Tell the compiler that tags between 24 and 253 are impossible.
239			if tag > INLINE_CAP as u8 && tag != TAG_STATIC && tag != TAG_BOXED {
240				std::hint::unreachable_unchecked();
241			}
242			// Check if the string is inline.
243			let is_inline = tag <= INLINE_CAP as u8;
244			// Get the length of the string.
245			let len = if is_inline {
246				tag as usize
247			} else {
248				self.data.heap.len
249			};
250			// Get the pointer to the string.
251			let ptr = if is_inline {
252				self.data.inline.as_ptr()
253			} else {
254				self.data.heap.ptr
255			};
256			// Return the string.
257			std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len))
258		}
259	}
260
261	/// Byte length of the string.
262	#[inline]
263	pub fn len(&self) -> usize {
264		self.as_str().len()
265	}
266
267	/// Whether the string is empty.
268	#[inline]
269	pub fn is_empty(&self) -> bool {
270		self.as_str().is_empty()
271	}
272
273	/// Convert into an owned `String`, copying the bytes.
274	#[inline]
275	pub fn into_string(self) -> String {
276		self.as_str().to_owned()
277	}
278}
279
280// -----------------------------------------------------------------------
281// Drop
282// -----------------------------------------------------------------------
283
284impl Drop for Strand {
285	#[inline]
286	fn drop(&mut self) {
287		// SAFETY: We only drop the inner Box<str> if the tag indicates it is Boxed.
288		// The pointer and length are guaranteed to be valid because they were created
289		// from a valid Box<str> in the `From<Box<str>>` implementation.
290		unsafe {
291			if self.data.inline[23] == TAG_BOXED {
292				let ptr = self.data.heap.ptr as *mut u8;
293				let len = self.data.heap.len;
294				let slice = std::ptr::slice_from_raw_parts_mut(ptr, len);
295				let _ = Box::from_raw(slice as *mut str);
296			}
297		}
298	}
299}
300
301// -----------------------------------------------------------------------
302// Clone
303// -----------------------------------------------------------------------
304
305impl Clone for Strand {
306	#[inline]
307	fn clone(&self) -> Self {
308		// SAFETY: We explicitly check the tag to see if it is Boxed.
309		// If it is, we perform a deep copy by allocating a new Box<str>.
310		// If it is Inline or Static, we can safely perform a bitwise copy because
311		// neither variant owns any heap allocations that need to be duplicated.
312		unsafe {
313			let tag = self.data.inline[23];
314			if tag == TAG_BOXED {
315				#[cold]
316				#[inline(never)]
317				fn cold_clone(s: &str) -> Strand {
318					Strand::from(Box::from(s))
319				}
320				cold_clone(self.as_str())
321			} else {
322				// For Inline and Static, it's just a bitwise copy
323				std::ptr::read(self as *const Strand)
324			}
325		}
326	}
327}
328
329// -----------------------------------------------------------------------
330// Default / Deref / AsRef / Borrow
331// -----------------------------------------------------------------------
332
333impl Default for Strand {
334	#[inline]
335	fn default() -> Self {
336		Self {
337			data: StrandData {
338				inline: [0u8; 24],
339			},
340		}
341	}
342}
343
344impl Deref for Strand {
345	type Target = str;
346	#[inline]
347	fn deref(&self) -> &str {
348		self.as_str()
349	}
350}
351
352impl AsRef<str> for Strand {
353	#[inline]
354	fn as_ref(&self) -> &str {
355		self.as_str()
356	}
357}
358
359impl Borrow<str> for Strand {
360	#[inline]
361	fn borrow(&self) -> &str {
362		self.as_str()
363	}
364}
365
366// -----------------------------------------------------------------------
367// Construction conversions
368// -----------------------------------------------------------------------
369
370impl From<&str> for Strand {
371	#[inline]
372	fn from(s: &str) -> Self {
373		Self::new(s)
374	}
375}
376
377impl From<String> for Strand {
378	#[inline]
379	fn from(s: String) -> Self {
380		if s.len() <= INLINE_CAP {
381			Self::new_inline(&s)
382		} else {
383			Self::from(s.into_boxed_str())
384		}
385	}
386}
387
388impl From<&String> for Strand {
389	#[inline]
390	fn from(s: &String) -> Self {
391		Self::new(s.as_str())
392	}
393}
394
395impl From<Box<str>> for Strand {
396	#[inline]
397	fn from(s: Box<str>) -> Self {
398		if s.len() <= INLINE_CAP {
399			Self::new_inline(&s)
400		} else {
401			let ptr = s.as_ptr();
402			let len = s.len();
403			std::mem::forget(s);
404			Self {
405				data: StrandData {
406					heap: ManuallyDrop::new(HeapData {
407						ptr,
408						len,
409						_pad: [0; HEAP_PAD_LEN],
410						tag: TAG_BOXED,
411					}),
412				},
413			}
414		}
415	}
416}
417
418impl From<Strand> for String {
419	#[inline]
420	fn from(s: Strand) -> String {
421		s.as_str().to_owned()
422	}
423}
424
425impl From<&Strand> for String {
426	#[inline]
427	fn from(s: &Strand) -> String {
428		s.as_str().to_owned()
429	}
430}
431
432// -----------------------------------------------------------------------
433// Equality / ordering / hashing
434// -----------------------------------------------------------------------
435
436impl Eq for Strand {}
437
438impl PartialEq for Strand {
439	#[inline]
440	fn eq(&self, other: &Self) -> bool {
441		// SAFETY: We only compare the arrays directly if both tags are <= INLINE_CAP.
442		// When an inline string is created in `new_inline`, the entire 24-byte array
443		// is zero-initialized before the string data is copied into it.
444		// Therefore, any unused padding bytes are guaranteed to be zero, making a
445		// direct byte-for-byte comparison of the full 24-byte array safe and correct.
446		unsafe {
447			// Get the strand tags
448			let tag_a = self.data.inline[23];
449			let tag_b = other.data.inline[23];
450			// Fast path: Both are Inline strings
451			if tag_a <= INLINE_CAP as u8 && tag_b <= INLINE_CAP as u8 {
452				// We can compare the 24-byte arrays directly
453				return self.data.inline == other.data.inline;
454			}
455		}
456		// Slow path: Types are different
457		self.as_str() == other.as_str()
458	}
459}
460
461impl PartialEq<str> for Strand {
462	#[inline]
463	fn eq(&self, other: &str) -> bool {
464		self.as_str() == other
465	}
466}
467
468impl PartialEq<&str> for Strand {
469	#[inline]
470	fn eq(&self, other: &&str) -> bool {
471		self.as_str() == *other
472	}
473}
474
475impl PartialEq<String> for Strand {
476	#[inline]
477	fn eq(&self, other: &String) -> bool {
478		self.as_str() == other.as_str()
479	}
480}
481
482impl Ord for Strand {
483	#[inline]
484	fn cmp(&self, other: &Self) -> Ordering {
485		// SAFETY: We only extract slices from the inline array if both tags are <= INLINE_CAP.
486		// We use the tag as the exact length of the valid string data, ensuring we don't
487		// compare any unused padding bytes which could interfere with lexicographical ordering.
488		unsafe {
489			// Get the strand tags
490			let tag_a = self.data.inline[23];
491			let tag_b = other.data.inline[23];
492			// Fast path: Both are Inline strings
493			if tag_a <= INLINE_CAP as u8 && tag_b <= INLINE_CAP as u8 {
494				// Get the lengths of the strings
495				let len_a = tag_a as usize;
496				let len_b = tag_b as usize;
497				// For ordering, we must compare the valid bytes exactly because
498				// the padding bytes might interfere with lexicographical ordering.
499				let slice_a = std::slice::from_raw_parts(self.data.inline.as_ptr(), len_a);
500				let slice_b = std::slice::from_raw_parts(other.data.inline.as_ptr(), len_b);
501				// Compare the strings
502				return slice_a.cmp(slice_b);
503			}
504		}
505		// Slow path: Types are different
506		self.as_str().cmp(other.as_str())
507	}
508}
509
510impl PartialOrd for Strand {
511	#[inline]
512	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
513		Some(self.cmp(other))
514	}
515}
516
517impl PartialOrd<str> for Strand {
518	#[inline]
519	fn partial_cmp(&self, other: &str) -> Option<Ordering> {
520		self.as_str().partial_cmp(other)
521	}
522}
523
524impl PartialOrd<String> for Strand {
525	#[inline]
526	fn partial_cmp(&self, other: &String) -> Option<Ordering> {
527		self.as_str().partial_cmp(other.as_str())
528	}
529}
530
531impl Hash for Strand {
532	#[inline]
533	fn hash<H: Hasher>(&self, state: &mut H) {
534		self.as_str().hash(state)
535	}
536}
537
538impl Debug for Strand {
539	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540		Debug::fmt(self.as_str(), f)
541	}
542}
543
544impl Display for Strand {
545	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546		Display::fmt(self.as_str(), f)
547	}
548}
549
550// -----------------------------------------------------------------------
551// serde
552// -----------------------------------------------------------------------
553
554impl Serialize for Strand {
555	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
556	where
557		S: Serializer,
558	{
559		serializer.serialize_str(self.as_str())
560	}
561}
562
563impl<'de> Deserialize<'de> for Strand {
564	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
565	where
566		D: Deserializer<'de>,
567	{
568		struct StrandVisitor;
569
570		impl<'de> Visitor<'de> for StrandVisitor {
571			type Value = Strand;
572
573			fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
574				f.write_str("a string")
575			}
576
577			fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
578				Ok(Strand::from(v))
579			}
580
581			fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
582				Ok(Strand::from(v))
583			}
584		}
585
586		deserializer.deserialize_str(StrandVisitor)
587	}
588}
589
590// -----------------------------------------------------------------------
591// revision
592// -----------------------------------------------------------------------
593
594impl Revisioned for Strand {
595	#[inline]
596	fn revision() -> u16 {
597		1
598	}
599}
600
601impl SerializeRevisioned for Strand {
602	#[inline]
603	fn serialize_revisioned<W: std::io::Write>(&self, writer: &mut W) -> Result<(), Error> {
604		self.as_str().serialize_revisioned(writer)
605	}
606}
607
608impl DeserializeRevisioned for Strand {
609	#[inline]
610	fn deserialize_revisioned<R: std::io::Read>(reader: &mut R) -> Result<Self, Error> {
611		let len = usize::deserialize_revisioned(reader)?;
612		if len == 0 {
613			return Ok(Self::default());
614		}
615		if len <= INLINE_CAP {
616			let mut inline = [0u8; 24];
617			reader.read_exact(&mut inline[..len]).map_err(Error::Io)?;
618			std::str::from_utf8(&inline[..len]).map_err(Error::Utf8Error)?;
619			inline[23] = len as u8;
620			return Ok(Strand {
621				data: StrandData {
622					inline,
623				},
624			});
625		}
626		let mut buf = vec![0u8; len];
627		reader.read_exact(&mut buf).map_err(Error::Io)?;
628		let s = String::from_utf8(buf).map_err(|e| Error::Utf8Error(e.utf8_error()))?;
629		Ok(Strand::from(s.into_boxed_str()))
630	}
631}
632
633impl revision::SkipRevisioned for Strand {
634	#[inline]
635	fn skip_revisioned<R: std::io::Read>(reader: &mut R) -> Result<(), Error> {
636		<String as revision::SkipRevisioned>::skip_revisioned(reader)
637	}
638
639	#[inline]
640	fn skip_revisioned_slice(reader: &mut revision::SliceReader<'_>) -> Result<(), Error> {
641		<String as revision::SkipRevisioned>::skip_revisioned_slice(reader)
642	}
643}
644
645impl revision::WalkRevisioned for Strand {
646	type Walker<'r, R: revision::BorrowedReader + 'r> = revision::LeafWalker<'r, Strand, R>;
647
648	#[inline]
649	fn walk_revisioned<'r, R: revision::BorrowedReader>(
650		reader: &'r mut R,
651	) -> Result<Self::Walker<'r, R>, Error> {
652		Ok(revision::LeafWalker::new(reader))
653	}
654}
655
656impl revision::LengthPrefixedBytes for Strand {}
657
658// -----------------------------------------------------------------------
659// storekey
660// -----------------------------------------------------------------------
661
662impl<F> storekey::Encode<F> for Strand {
663	#[inline]
664	fn encode<W: std::io::Write>(
665		&self,
666		writer: &mut storekey::Writer<W>,
667	) -> Result<(), storekey::EncodeError> {
668		<str as storekey::Encode<F>>::encode(self.as_str(), writer)
669	}
670}
671
672impl<'de, F> storekey::BorrowDecode<'de, F> for Strand {
673	#[inline]
674	fn borrow_decode(
675		reader: &mut storekey::BorrowReader<'de>,
676	) -> Result<Self, storekey::DecodeError> {
677		let cow = reader.read_str_cow()?;
678		let s: &str = &cow;
679		Ok(if s.len() <= INLINE_CAP {
680			Self::new_inline(s)
681		} else {
682			Self::from(Box::from(s))
683		})
684	}
685}
686
687impl<F> storekey::Decode<F> for Strand {
688	#[inline]
689	fn decode<R: std::io::BufRead>(
690		reader: &mut storekey::Reader<R>,
691	) -> Result<Self, storekey::DecodeError> {
692		let bytes = reader.read_vec()?;
693		let s = std::str::from_utf8(&bytes).map_err(|_| storekey::DecodeError::Utf8)?;
694		Ok(if s.len() <= INLINE_CAP {
695			Self::new_inline(s)
696		} else {
697			Self::from(Box::from(s))
698		})
699	}
700}
701
702// -----------------------------------------------------------------------
703// arbitrary
704// -----------------------------------------------------------------------
705
706#[cfg(feature = "arbitrary")]
707impl<'a> arbitrary::Arbitrary<'a> for Strand {
708	#[inline]
709	fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
710		let s = <&str as arbitrary::Arbitrary<'a>>::arbitrary(u)?;
711		Ok(Strand::from(s))
712	}
713
714	#[inline]
715	fn size_hint(depth: usize) -> (usize, Option<usize>) {
716		<&str as arbitrary::Arbitrary<'a>>::size_hint(depth)
717	}
718}
719
720mod table;
721
722pub use table::TableName;
723
724#[cfg(test)]
725mod tests {
726	use super::*;
727
728	const SHORT: &str = "hello";
729	/// 23 bytes — the inline boundary, exactly fills the inline buffer.
730	const AT_CAP: &str = "abcdefghijklmnopqrstuvw";
731	const LONG: &str = "this string is intentionally much longer than twenty three bytes so it must live on the heap";
732
733	// --- layout ---------------------------------------------------------
734
735	#[test]
736	fn stack_size_is_24_bytes() {
737		use std::mem::size_of;
738		// Note: Strand is always 24 bytes.
739		// String is 24 bytes on 64-bit
740		// String is 12 bytes on 32-bit.
741		assert_eq!(size_of::<Strand>(), 24);
742	}
743
744	// --- basic construction --------------------------------------------
745
746	#[test]
747	fn short_strings_are_inline() {
748		let s = Strand::from(SHORT);
749		assert!(s.is_inline());
750		assert_eq!(s.as_str(), SHORT);
751	}
752
753	#[test]
754	fn long_strings_are_boxed() {
755		let s = Strand::from(LONG);
756		assert!(!s.is_inline());
757		assert!(!s.is_static());
758		assert!(s.is_boxed());
759		assert_eq!(s.as_str(), LONG);
760	}
761
762	#[test]
763	fn inline_boundary() {
764		assert_eq!(AT_CAP.len(), INLINE_CAP);
765		assert!(Strand::from(AT_CAP).is_inline());
766
767		let over: String = "a".repeat(INLINE_CAP + 1);
768		assert!(!Strand::from(over.as_str()).is_inline());
769	}
770
771	#[test]
772	fn empty_is_inline() {
773		let s = Strand::from("");
774		assert!(s.is_inline());
775		assert!(s.is_empty());
776		assert_eq!(s.as_str(), "");
777	}
778
779	#[test]
780	fn default_is_empty_inline() {
781		let s = Strand::default();
782		assert!(s.is_inline());
783		assert!(s.is_empty());
784	}
785
786	// --- clone & drop --------------------------------------------------
787
788	#[test]
789	fn inline_clone_is_independent_copy() {
790		let s = Strand::from(SHORT);
791		let t = s.clone();
792		assert_eq!(s.as_str(), t.as_str());
793		// Both are inline; drop one and the other must still be valid.
794		drop(s);
795		assert_eq!(t.as_str(), SHORT);
796	}
797
798	#[test]
799	fn boxed_clone_is_deep_copy() {
800		let s = Strand::from(LONG);
801		assert!(s.is_boxed());
802		let t = s.clone();
803		assert_eq!(s.as_str(), t.as_str());
804		// Each `Boxed` clone owns its own allocation; the byte
805		// pointers must differ even though the contents are equal.
806		assert_ne!(s.as_str().as_ptr(), t.as_str().as_ptr());
807		// Drop the original — the clone must still be fully valid.
808		drop(s);
809		assert_eq!(t.as_str(), LONG);
810	}
811
812	// --- static --------------------------------------------------------
813
814	#[test]
815	fn new_static_is_static() {
816		let s = Strand::new_static("foo");
817		assert!(s.is_static());
818		assert!(!s.is_inline());
819		assert!(!s.is_boxed());
820		assert_eq!(s.as_str(), "foo");
821	}
822
823	#[test]
824	fn new_static_long_is_still_static() {
825		// Longer than `INLINE_CAP`: the key benefit of `Static` is
826		// that it skips the allocation regardless of length.
827		let s = Strand::new_static(LONG);
828		assert!(s.len() > INLINE_CAP);
829		assert!(s.is_static());
830		assert!(!s.is_boxed());
831		assert_eq!(s.as_str(), LONG);
832	}
833
834	#[test]
835	fn new_static_is_const() {
836		// The whole point of the variant: `const` construction,
837		// including for strings longer than `INLINE_CAP`.
838		const SHORT_STATIC: Strand = Strand::new_static("foo");
839		const LONG_STATIC: Strand =
840			Strand::new_static("this literal is longer than INLINE_CAP but costs nothing");
841		assert_eq!(SHORT_STATIC.as_str(), "foo");
842		assert!(LONG_STATIC.as_str().len() > INLINE_CAP);
843	}
844
845	#[test]
846	fn static_clone_is_pointer_copy() {
847		let original = "some compile-time string";
848		let s = Strand::new_static(original);
849		let t = s.clone();
850		// Both clones must point at the exact same backing bytes as
851		// the original literal — no allocation, no copy.
852		assert_eq!(s.as_str().as_ptr(), original.as_ptr());
853		assert_eq!(t.as_str().as_ptr(), original.as_ptr());
854	}
855
856	// --- semantics -----------------------------------------------------
857
858	#[test]
859	fn cross_repr_equality() {
860		// All three variants holding the same bytes must compare
861		// equal to each other.
862		let inline = Strand::from("abc");
863		let stat = Strand::new_static("abc");
864		let boxed = Strand::from("a".repeat(INLINE_CAP + 1));
865		let boxed2 = Strand::from(boxed.as_str());
866		assert!(inline.is_inline());
867		assert!(stat.is_static());
868		assert!(boxed.is_boxed());
869		assert_eq!(inline, stat);
870		assert_eq!(stat, inline);
871		assert_eq!(boxed, boxed2);
872	}
873
874	#[test]
875	fn ord_is_lexicographic() {
876		let a = Strand::from("apple");
877		let b = Strand::from("banana");
878		assert!(a < b);
879	}
880
881	#[test]
882	fn hashing_works_as_map_key() {
883		use std::collections::HashMap;
884		let mut m = HashMap::new();
885		m.insert(Strand::from("k"), 1);
886		assert_eq!(m.get("k"), Some(&1));
887	}
888
889	#[test]
890	fn roundtrip_revisioned() {
891		let s = Strand::from("round trip");
892		let mut bytes = Vec::new();
893		s.serialize_revisioned(&mut bytes).unwrap();
894		let back = Strand::deserialize_revisioned(&mut bytes.as_slice()).unwrap();
895		assert_eq!(s, back);
896	}
897
898	#[test]
899	fn roundtrip_long_heap() {
900		let s = Strand::from(LONG);
901		let mut bytes = Vec::new();
902		s.serialize_revisioned(&mut bytes).unwrap();
903		let back = Strand::deserialize_revisioned(&mut bytes.as_slice()).unwrap();
904		assert_eq!(s, back);
905		assert!(!back.is_inline());
906	}
907
908	// --- revisioned edge cases ----------------------------------------
909	//
910	// These exercise the in-place `Box<str>` decode path and the
911	// inline-buffer decode path, which bypass `String::deserialize_revisioned`
912	// entirely and must cover the same cases the generic `String` impl
913	// used to.
914
915	fn roundtrip_revisioned_for(s: &str, expect_inline: bool) {
916		let strand = Strand::from(s);
917		let mut bytes = Vec::new();
918		strand.serialize_revisioned(&mut bytes).unwrap();
919		let back = Strand::deserialize_revisioned(&mut bytes.as_slice()).unwrap();
920		assert_eq!(back.as_str(), s);
921		assert_eq!(back.is_inline(), expect_inline);
922	}
923
924	#[test]
925	fn roundtrip_revisioned_empty() {
926		roundtrip_revisioned_for("", true);
927	}
928
929	#[test]
930	fn roundtrip_revisioned_at_inline_cap() {
931		roundtrip_revisioned_for(AT_CAP, true);
932		assert_eq!(AT_CAP.len(), INLINE_CAP);
933	}
934
935	#[test]
936	fn roundtrip_revisioned_one_over_inline_cap() {
937		let over: String = "a".repeat(INLINE_CAP + 1);
938		roundtrip_revisioned_for(&over, false);
939	}
940
941	/// Multi-byte UTF-8 that straddles the inline/heap boundary — the
942	/// in-place decode must not be fooled by a codepoint whose UTF-8
943	/// length is not 1, and must validate UTF-8 even when the bytes
944	/// are written directly into a freshly allocated `Vec<u8>`.
945	#[test]
946	fn roundtrip_revisioned_utf8_heap() {
947		let s = "δοκιμή αξιολόγησης κειμένου με πολυβυτικούς χαρακτήρες";
948		assert!(s.len() > INLINE_CAP);
949		roundtrip_revisioned_for(s, false);
950	}
951
952	#[test]
953	fn deserialize_revisioned_rejects_invalid_utf8() {
954		// Manually craft a payload that claims 2 bytes of payload but
955		// provides invalid UTF-8 (a lone continuation byte).
956		let mut bytes = Vec::new();
957		2usize.serialize_revisioned(&mut bytes).unwrap();
958		bytes.push(0xFF);
959		bytes.push(0xFE);
960		assert!(Strand::deserialize_revisioned(&mut bytes.as_slice()).is_err());
961	}
962
963	// --- storekey round-trips -----------------------------------------
964
965	fn roundtrip_storekey_for(s: &str, expect_inline: bool) {
966		use storekey::{BorrowDecode, Decode, Encode};
967
968		let strand = Strand::from(s);
969		// Encode via storekey.
970		let mut buf = Vec::new();
971		let mut w = storekey::Writer::new(&mut buf);
972		<Strand as Encode<()>>::encode(&strand, &mut w).unwrap();
973
974		// BorrowDecode path.
975		{
976			let mut r = storekey::BorrowReader::new(&buf);
977			let back = <Strand as BorrowDecode<'_, ()>>::borrow_decode(&mut r).unwrap();
978			assert_eq!(back.as_str(), s);
979			assert_eq!(back.is_inline(), expect_inline);
980		}
981
982		// Streaming Decode path.
983		{
984			let mut r = storekey::Reader::new(buf.as_slice());
985			let back = <Strand as Decode<()>>::decode(&mut r).unwrap();
986			assert_eq!(back.as_str(), s);
987			assert_eq!(back.is_inline(), expect_inline);
988		}
989	}
990
991	#[test]
992	fn roundtrip_storekey_empty() {
993		roundtrip_storekey_for("", true);
994	}
995
996	#[test]
997	fn roundtrip_storekey_short() {
998		roundtrip_storekey_for(SHORT, true);
999	}
1000
1001	#[test]
1002	fn roundtrip_storekey_at_inline_cap() {
1003		roundtrip_storekey_for(AT_CAP, true);
1004	}
1005
1006	#[test]
1007	fn roundtrip_storekey_long() {
1008		roundtrip_storekey_for(LONG, false);
1009	}
1010
1011	/// Values that contain `0x00` and `0x01` bytes exercise the
1012	/// escape-aware decoder branch in `BorrowReader::read_str_cow`
1013	/// (which returns `Cow::Owned` instead of `Cow::Borrowed`).
1014	#[test]
1015	fn roundtrip_storekey_with_escape_bytes() {
1016		roundtrip_storekey_for("abc\0def\x01ghi", true);
1017		let long_with_escapes: String = format!("{}\0{}", "x".repeat(30), "y".repeat(30));
1018		roundtrip_storekey_for(&long_with_escapes, false);
1019	}
1020
1021	// --- wire-format compatibility with `String` ----------------------
1022	//
1023	// `Strand` is a drop-in replacement for `String` at both the
1024	// `revision` (on-disk, document/change-feed) and `storekey`
1025	// (index-key) layers. The entire value of the small-string
1026	// optimisation hinges on that being invisible from the wire
1027	// format's perspective — any byte-level divergence between
1028	// `Strand::serialize` and `String::serialize` for the same input
1029	// would silently break on-disk data on upgrade. These tests assert
1030	// both byte-identity and cross-type decode compatibility for
1031	// every edge case the earlier roundtrip tests touch.
1032
1033	/// Inputs that exercise every interesting structural case:
1034	/// - empty (length-prefix only, no payload);
1035	/// - one byte below, equal to, and one byte above `INLINE_CAP` (the inline/heap boundary only
1036	///   the `Strand` impl cares about; from the wire's perspective it should be invisible);
1037	/// - a long ASCII string (typical `LONG` payload);
1038	/// - multi-byte UTF-8 whose byte length straddles `INLINE_CAP` (guards against off-by-one in
1039	///   the boundary check or a codepoint-vs-byte confusion);
1040	/// - strings containing `0x00` and `0x01` bytes, which trigger the escape-aware branch in
1041	///   `storekey`'s writer/reader.
1042	fn wire_format_cases() -> Vec<String> {
1043		let at_cap_minus_one: String = "a".repeat(INLINE_CAP - 1);
1044		let at_cap: String = "a".repeat(INLINE_CAP);
1045		let at_cap_plus_one: String = "a".repeat(INLINE_CAP + 1);
1046		let utf8_heap = "δοκιμή αξιολόγησης κειμένου με πολυβυτικούς χαρακτήρες".to_owned();
1047		let escape_short = "abc\0def\x01ghi".to_owned();
1048		let escape_long = format!("{}\0{}", "x".repeat(30), "y".repeat(30));
1049		vec![
1050			String::new(),
1051			SHORT.to_owned(),
1052			at_cap_minus_one,
1053			at_cap,
1054			at_cap_plus_one,
1055			LONG.to_owned(),
1056			utf8_heap,
1057			escape_short,
1058			escape_long,
1059		]
1060	}
1061
1062	/// For every fixture, assert that `Strand` produces byte-identical
1063	/// `revisioned` output to `String`, and that both types can decode
1064	/// each other's bytes back to the original value.
1065	#[test]
1066	fn revisioned_wire_matches_string() {
1067		for input in wire_format_cases() {
1068			let strand = Strand::from(input.as_str());
1069
1070			let mut strand_bytes = Vec::new();
1071			strand.serialize_revisioned(&mut strand_bytes).unwrap();
1072
1073			let mut string_bytes = Vec::new();
1074			input.serialize_revisioned(&mut string_bytes).unwrap();
1075
1076			// (1) Byte-identical output.
1077			assert_eq!(
1078				strand_bytes, string_bytes,
1079				"Strand and String must produce identical revisioned bytes for {:?}",
1080				input
1081			);
1082
1083			// (2) `Strand` can decode bytes produced by `String`.
1084			let from_string_bytes =
1085				Strand::deserialize_revisioned(&mut string_bytes.as_slice()).unwrap();
1086			assert_eq!(from_string_bytes.as_str(), input);
1087
1088			// (3) `String` can decode bytes produced by `Strand`.
1089			let from_strand_bytes =
1090				String::deserialize_revisioned(&mut strand_bytes.as_slice()).unwrap();
1091			assert_eq!(from_strand_bytes, input);
1092		}
1093	}
1094
1095	/// [`revision::LengthPrefixedBytes`] enables [`revision::LeafWalker::with_bytes`] on
1096	/// slice-backed readers; payload bytes must match UTF-8 encoding of the strand.
1097	#[test]
1098	fn revision_leaf_walker_with_bytes_matches_strand_utf8() {
1099		use revision::{SerializeRevisioned, WalkRevisioned};
1100		let s = Strand::from("hello ρ");
1101		let mut buf = Vec::new();
1102		s.serialize_revisioned(&mut buf).unwrap();
1103		let mut r = buf.as_slice();
1104		let w = Strand::walk_revisioned(&mut r).unwrap();
1105		w.with_bytes(|bytes| assert_eq!(bytes, s.as_str().as_bytes())).unwrap();
1106	}
1107
1108	/// Same assertions for the `storekey` encoding, covering both the
1109	/// borrowed and streaming decode paths plus the cross-type decode.
1110	#[test]
1111	fn storekey_wire_matches_string() {
1112		use storekey::{BorrowDecode, Decode, Encode};
1113
1114		for input in wire_format_cases() {
1115			let strand = Strand::from(input.as_str());
1116
1117			let mut strand_bytes = Vec::new();
1118			{
1119				let mut w = storekey::Writer::new(&mut strand_bytes);
1120				<Strand as Encode<()>>::encode(&strand, &mut w).unwrap();
1121			}
1122
1123			let mut string_bytes = Vec::new();
1124			{
1125				let mut w = storekey::Writer::new(&mut string_bytes);
1126				<String as Encode<()>>::encode(&input, &mut w).unwrap();
1127			}
1128
1129			// (1) Byte-identical output — escape-aware encoder must
1130			// treat `Strand` exactly like `String`, so `0x00`/`0x01`
1131			// bytes come out escaped the same way.
1132			assert_eq!(
1133				strand_bytes, string_bytes,
1134				"Strand and String must produce identical storekey bytes for {:?}",
1135				input
1136			);
1137
1138			// (2) `Strand` (both BorrowDecode and Decode) can decode
1139			// bytes produced by `String`.
1140			{
1141				let mut r = storekey::BorrowReader::new(&string_bytes);
1142				let back = <Strand as BorrowDecode<'_, ()>>::borrow_decode(&mut r).unwrap();
1143				assert_eq!(back.as_str(), input);
1144			}
1145			{
1146				let mut r = storekey::Reader::new(string_bytes.as_slice());
1147				let back = <Strand as Decode<()>>::decode(&mut r).unwrap();
1148				assert_eq!(back.as_str(), input);
1149			}
1150
1151			// (3) `String` can decode bytes produced by `Strand`.
1152			{
1153				let mut r = storekey::BorrowReader::new(&strand_bytes);
1154				let back = <String as BorrowDecode<'_, ()>>::borrow_decode(&mut r).unwrap();
1155				assert_eq!(back, input);
1156			}
1157			{
1158				let mut r = storekey::Reader::new(strand_bytes.as_slice());
1159				let back = <String as Decode<()>>::decode(&mut r).unwrap();
1160				assert_eq!(back, input);
1161			}
1162		}
1163	}
1164}