tuwunel_core/utils/
string.rs1mod 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#[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#[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)] pub 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#[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
147pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
149 let str: &str = str_from_bytes(bytes)?;
150 Ok(str.to_owned())
151}
152
153#[inline]
155pub fn str_from_bytes(bytes: &[u8]) -> Result<&str> { Ok(std::str::from_utf8(bytes)?) }