bp3d_util/format.rs
1// Copyright (c) 2025, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13// * Neither the name of BlockProject 3D nor the names of its contributors
14// may be used to endorse or promote products derived from this software
15// without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29//! Formatting utilities.
30
31use std::mem::MaybeUninit;
32
33/// Fixed length string buffer.
34#[derive(Clone, Debug)]
35pub struct FixedBufStr<const N: usize> {
36 len: usize,
37 buffer: [MaybeUninit<u8>; N],
38}
39
40impl<const N: usize> Default for FixedBufStr<N> {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46// This function is full of unsafe because it ran slower than expected.
47// It appears that even a single subtraction has a HUGE impact on performance in Rust.
48// It also appears that having this as a function instead of being inlined multiplies by 2 running
49// time.
50// Unfortunately that thing is in a hot path within debug.tracing.
51#[inline(always)]
52fn utf8_max(buf: &[u8], max: usize) -> usize {
53 let buf_len = buf.len();
54 if buf_len <= max {
55 buf_len
56 } else if max == 0 {
57 0
58 } else if unsafe { buf.get_unchecked(max.unchecked_sub(1)) } & 0x80 == 0x00 {
59 max
60 } else {
61 let start = unsafe { max.unchecked_sub(1) };
62 let mut i = start;
63 unsafe {
64 while buf.get_unchecked(i) & 0xC0 == 0x80 {
65 i = i.unchecked_sub(1);
66 }
67 let n = start.unchecked_sub(i);
68 if (buf.get_unchecked(i) & 0xF0 == 0xF0 && n == 4)
69 || (buf.get_unchecked(i) & 0xE0 == 0xE0 && n == 3)
70 || (buf.get_unchecked(i) & 0xC0 == 0xC0 && n == 2)
71 {
72 max
73 } else {
74 i
75 }
76 }
77 }
78}
79
80impl<const N: usize> FixedBufStr<N> {
81 /// Creates a new fixed length string buffer.
82 pub fn new() -> FixedBufStr<N> {
83 FixedBufStr {
84 buffer: unsafe { MaybeUninit::uninit().assume_init() },
85 len: 0,
86 }
87 }
88
89 /// Extracts the string from this buffer.
90 //type inference works so why should the code look awfully more complex?
91 #[allow(clippy::missing_transmute_annotations)]
92 pub fn str(&self) -> &str {
93 unsafe { std::str::from_utf8_unchecked(std::mem::transmute(&self.buffer[..self.len as _])) }
94 }
95
96 /// Constructs this buffer from an existing string.
97 //type inference works so why should the code look awfully more complex?
98 #[allow(clippy::missing_transmute_annotations)]
99 //I believe this is a false-positive, FromStr returns a Result not Self.
100 #[allow(clippy::should_implement_trait)]
101 pub fn from_str(value: &str) -> Self {
102 let mut buffer = FixedBufStr::new();
103 let len = utf8_max(value.as_bytes(), N);
104 unsafe {
105 std::ptr::copy_nonoverlapping(
106 value.as_ptr(),
107 std::mem::transmute(buffer.buffer.as_mut_ptr()),
108 len,
109 );
110 }
111 buffer.len = len as _;
112 buffer
113 }
114
115 /// Appends a raw byte buffer at the end of this string buffer.
116 ///
117 /// Returns the number of bytes written.
118 ///
119 /// # Arguments
120 ///
121 /// * `buf`: the raw byte buffer to append.
122 ///
123 /// returns: usize
124 ///
125 /// # Safety
126 ///
127 /// * [FixedBufStr](FixedBufStr) contains only valid UTF-8 strings so buf must contain only valid UTF-8
128 /// bytes.
129 /// * If buf contains invalid UTF-8 bytes, further operations on the log message buffer may
130 /// result in UB.
131 //type inference works so why should the code look awfully more complex?
132 #[allow(clippy::missing_transmute_annotations)]
133 pub unsafe fn write(&mut self, buf: &[u8]) -> usize {
134 let len = utf8_max(buf, N - self.len);
135 unsafe {
136 std::ptr::copy_nonoverlapping(
137 buf.as_ptr(),
138 std::mem::transmute(self.buffer.as_mut_ptr().add(self.len)),
139 len,
140 );
141 }
142 self.len += len;
143 len
144 }
145}
146
147impl<const N: usize> std::fmt::Write for FixedBufStr<N> {
148 fn write_str(&mut self, value: &str) -> std::fmt::Result {
149 unsafe { self.write(value.as_bytes()) };
150 Ok(())
151 }
152}
153
154/// An io [Write](std::io::Write) to fmt [Write](std::fmt::Write).
155///
156/// This may look like a hack but is a requirement for pathological APIs such as presented by the
157/// time crate.
158pub struct IoToFmt<W: std::fmt::Write>(W);
159
160impl<W: std::fmt::Write> IoToFmt<W> {
161 /// Create a new [IoToFmt](IoToFmt) wrapper.
162 ///
163 /// # Arguments
164 ///
165 /// * `w`: target fmt [Write](std::fmt::Write) to write into.
166 ///
167 /// returns: IoToFmt<W>
168 pub fn new(w: W) -> Self {
169 Self(w)
170 }
171
172 /// Extracts the underlying [Write](std::fmt::Write).
173 pub fn into_inner(self) -> W {
174 self.0
175 }
176}
177
178impl<W: std::fmt::Write> std::io::Write for IoToFmt<W> {
179 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
180 let str = std::str::from_utf8(buf)
181 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
182 self.0
183 .write_str(str)
184 .map(|_| str.len())
185 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
186 }
187
188 fn flush(&mut self) -> std::io::Result<()> {
189 Ok(())
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use crate::format::FixedBufStr;
196 use std::fmt::Write;
197
198 #[test]
199 fn basic() {
200 let mut msg: FixedBufStr<64> = FixedBufStr::new();
201 let _ = write!(msg, "this");
202 let _ = write!(msg, " is");
203 let _ = write!(msg, " a");
204 let _ = write!(msg, " test");
205 assert_eq!(msg.str(), "this is a test");
206 }
207
208 #[test]
209 fn truncate_ascii() {
210 let mut msg: FixedBufStr<4> = FixedBufStr::new();
211 let _ = write!(msg, "this");
212 let _ = write!(msg, " is");
213 let _ = write!(msg, " a");
214 let _ = write!(msg, " test");
215 assert_eq!(msg.str().len(), 4);
216 assert_eq!(msg.str(), "this");
217 }
218
219 #[test]
220 fn truncate_utf8_exact() {
221 let mut msg: FixedBufStr<3> = FixedBufStr::new();
222 let _ = write!(msg, "我");
223 assert_eq!(msg.str().len(), 3);
224 assert_eq!(msg.str(), "我");
225 }
226
227 #[test]
228 fn truncate_utf8_exact2() {
229 let mut msg: FixedBufStr<6> = FixedBufStr::new();
230 let _ = write!(msg, "我是");
231 assert_eq!(msg.str().len(), 6);
232 assert_eq!(msg.str(), "我是");
233 }
234
235 #[test]
236 fn truncate_utf8_exact3() {
237 let mut msg: FixedBufStr<6> = FixedBufStr::new();
238 let _ = write!(msg, "我abcd");
239 assert_eq!(msg.str().len(), 6);
240 assert_eq!(msg.str(), "我abc");
241 }
242
243 #[test]
244 fn truncate_utf8() {
245 let mut msg: FixedBufStr<4> = FixedBufStr::new();
246 let _ = write!(msg, "我是");
247 assert_eq!(msg.str().len(), 3);
248 assert_eq!(msg.str(), "我");
249 }
250}