Skip to main content

tuwunel_core/utils/
string.rs

1mod between;
2pub mod de;
3mod split;
4mod tests;
5mod unquote;
6mod unquoted;
7
8use std::{mem::replace, ops::Range};
9
10pub use self::{between::Between, split::SplitInfallible, unquote::Unquote, unquoted::Unquoted};
11use crate::{Result, smallstr::SmallString};
12
13pub const EMPTY: &str = "";
14
15/// Constant expression to bypass format! if the argument is a string literal
16/// but not a format string. If the literal is a format string then String is
17/// returned otherwise the input (i.e. &'static str) is returned. If multiple
18/// arguments are provided the first is assumed to be a format string.
19#[macro_export]
20#[collapse_debuginfo(yes)]
21macro_rules! format_maybe {
22	($s:literal $(,)?) => {
23		if $crate::is_format!($s) { std::format!($s).into() } else { $s.into() }
24	};
25
26	($s:literal, $($args:tt)+) => {
27		std::format!($s, $($args)+).into()
28	};
29}
30
31/// Constant expression to decide if a literal is a format string. Note: could
32/// use some improvement.
33#[macro_export]
34#[collapse_debuginfo(yes)]
35macro_rules! is_format {
36	($s:literal) => {
37		::const_str::contains!($s, "{") && ::const_str::contains!($s, "}")
38	};
39
40	($($s:tt)+) => {
41		false
42	};
43}
44
45#[inline]
46pub fn collect_stream<F>(func: F) -> Result<String>
47where
48	F: FnOnce(&mut dyn std::fmt::Write) -> Result,
49{
50	let mut out = String::new();
51	func(&mut out)?;
52	Ok(out)
53}
54
55#[inline]
56#[must_use]
57pub fn camel_to_snake_string(s: &str) -> String {
58	let est_len = s
59		.chars()
60		.fold(s.len(), |est, c| est.saturating_add(usize::from(c.is_ascii_uppercase())));
61
62	let mut ret = String::with_capacity(est_len);
63	camel_to_snake_case(&mut ret, s.as_bytes()).expect("string-to-string stream error");
64	ret
65}
66
67#[inline]
68#[expect(clippy::unbuffered_bytes)] // these are allocated string utilities, not file I/O utils
69pub fn camel_to_snake_case<I, O>(output: &mut O, input: I) -> Result
70where
71	I: std::io::Read,
72	O: std::fmt::Write,
73{
74	let mut state = false;
75	input
76		.bytes()
77		.take_while(Result::is_ok)
78		.map(Result::unwrap)
79		.map(char::from)
80		.try_for_each(|ch| {
81			let m = ch.is_ascii_uppercase();
82			let s = replace(&mut state, !m);
83			if m && s {
84				output.write_char('_')?;
85			}
86			output.write_char(ch.to_ascii_lowercase())?;
87			Result::<()>::Ok(())
88		})
89}
90
91/// Find the common prefix from a collection of strings and return a slice
92/// ```
93/// use tuwunel_core::utils::string::common_prefix;
94/// let input = ["conduwuit", "conduit", "construct"];
95/// common_prefix(&input) == "con";
96/// ```
97#[must_use]
98#[expect(clippy::string_slice)]
99pub fn common_prefix<T: AsRef<str>>(choice: &[T]) -> &str {
100	choice.first().map_or(EMPTY, move |best| {
101		choice
102			.iter()
103			.skip(1)
104			.fold(best.as_ref(), |best, choice| {
105				&best[0..choice
106					.as_ref()
107					.char_indices()
108					.zip(best.char_indices())
109					.take_while(|&(a, b)| a == b)
110					.count()]
111			})
112	})
113}
114
115#[inline]
116#[must_use]
117#[expect(clippy::arithmetic_side_effects)]
118pub fn truncate_deterministic(str: &str, range: Option<Range<usize>>) -> &str {
119	let range = range.unwrap_or(0..str.len());
120	let len = str
121		.as_bytes()
122		.iter()
123		.copied()
124		.map(Into::into)
125		.fold(0_usize, usize::wrapping_add)
126		.wrapping_rem(str.len().max(1))
127		.clamp(range.start, range.end);
128
129	str.char_indices()
130		.nth(len)
131		.map(|(i, _)| str.split_at(i).0)
132		.unwrap_or(str)
133}
134
135pub fn to_small_string<const CAP: usize, T>(t: T) -> SmallString<[u8; CAP]>
136where
137	T: std::fmt::Display,
138{
139	use std::fmt::Write;
140
141	let mut ret = SmallString::<[u8; CAP]>::new();
142	write!(&mut ret, "{t}").expect("Failed to Display type in SmallString");
143
144	ret
145}
146
147/// Parses the bytes into a string.
148pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
149	let str: &str = str_from_bytes(bytes)?;
150	Ok(str.to_owned())
151}
152
153/// Parses the bytes into a string.
154#[inline]
155pub fn str_from_bytes(bytes: &[u8]) -> Result<&str> { Ok(std::str::from_utf8(bytes)?) }