lx/
string.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4// UNSAFETY: Transmuting to implement from_bytes.
5#![expect(unsafe_code)]
6
7use std::borrow;
8use std::fmt::Write;
9use std::fmt::{self};
10use std::ops;
11use std::str;
12
13/// An owned string that may or may not be valid utf-8.
14///
15/// This is analogous to `OsString` on Linux, but behaves the same on all platforms.
16#[derive(Clone, Hash, PartialEq, Eq, Default)]
17pub struct LxString {
18    bytes: Vec<u8>,
19}
20
21impl LxString {
22    /// Creates an empty `LxString`.
23    pub fn new() -> Self {
24        Self { bytes: Vec::new() }
25    }
26
27    /// Creates a `LxString` from a byte vector.
28    pub fn from_vec(vec: Vec<u8>) -> Self {
29        Self { bytes: vec }
30    }
31
32    /// Creates a `LxString` with the specified capacity.
33    pub fn with_capacity(capacity: usize) -> Self {
34        Self {
35            bytes: Vec::with_capacity(capacity),
36        }
37    }
38
39    /// Yields the underlying byte vector of this `LxString`.
40    pub fn into_vec(self) -> Vec<u8> {
41        self.bytes
42    }
43
44    /// Converts the `LxString` into a `String` if it contains valid Unicode data.
45    pub fn into_string(self) -> Result<String, Self> {
46        String::from_utf8(self.bytes).map_err(|e| Self {
47            bytes: e.into_bytes(),
48        })
49    }
50
51    /// Converts to an `LxStr` slice.
52    pub fn as_lx_str(&self) -> &LxStr {
53        self
54    }
55
56    /// Returns the capacity of the `LxString`.
57    pub fn capacity(&self) -> usize {
58        self.bytes.capacity()
59    }
60
61    /// Clears the contents of the `LxString`.
62    pub fn clear(&mut self) {
63        self.bytes.clear()
64    }
65
66    /// Extends the string with the given `AsRef<LxStr>` slice.
67    pub fn push(&mut self, s: impl AsRef<LxStr>) {
68        self.bytes.extend_from_slice(&s.as_ref().bytes)
69    }
70
71    /// Reserves capacity for at least `additional` more capacity to be inserted into this string.
72    pub fn reserve(&mut self, additional: usize) {
73        self.bytes.reserve(additional);
74    }
75
76    /// Reserves the minimum capacity for exactly additional more capacity to be inserted into this string. Does nothing if
77    /// the capacity is already sufficient.
78    pub fn reserve_exact(&mut self, additional: usize) {
79        self.bytes.reserve_exact(additional);
80    }
81
82    /// Shrinks the capacity of this string to match its length.
83    pub fn shrink_to_fit(&mut self) {
84        self.bytes.shrink_to_fit()
85    }
86}
87
88impl ops::Deref for LxString {
89    type Target = LxStr;
90
91    fn deref(&self) -> &Self::Target {
92        LxStr::from_bytes(&self.bytes)
93    }
94}
95
96impl borrow::Borrow<LxStr> for LxString {
97    fn borrow(&self) -> &LxStr {
98        self
99    }
100}
101
102impl<T> From<&T> for LxString
103where
104    T: ?Sized + AsRef<LxStr>,
105{
106    fn from(s: &T) -> Self {
107        s.as_ref().to_lx_string()
108    }
109}
110
111impl From<String> for LxString {
112    fn from(s: String) -> Self {
113        LxString::from_vec(s.into())
114    }
115}
116
117impl PartialEq<LxString> for &str {
118    fn eq(&self, other: &LxString) -> bool {
119        **self == **other
120    }
121}
122
123impl PartialEq<LxString> for str {
124    fn eq(&self, other: &LxString) -> bool {
125        &**other == self
126    }
127}
128
129impl PartialEq<str> for LxString {
130    fn eq(&self, other: &str) -> bool {
131        &**self == other
132    }
133}
134
135impl PartialEq<&str> for LxString {
136    fn eq(&self, other: &&str) -> bool {
137        **self == **other
138    }
139}
140
141impl fmt::Debug for LxString {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        fmt::Debug::fmt(&**self, f)
144    }
145}
146
147/// A borrowed reference to a string that may or may not be valid utf-8.
148///
149/// This is analogous to `OsStr` on Linux, but behaves the same on all platforms.
150#[derive(PartialEq, Eq, Hash)]
151#[repr(transparent)]
152pub struct LxStr {
153    bytes: [u8],
154}
155
156impl LxStr {
157    /// Coerces into an `LxStr` slice.
158    pub fn new<T: AsRef<LxStr> + ?Sized>(s: &T) -> &LxStr {
159        s.as_ref()
160    }
161
162    /// Creates an `LxStr` from a byte slice.
163    pub fn from_bytes(slice: &[u8]) -> &LxStr {
164        // SAFETY: &LxStr has the same repr as &[u8], and doesn't add any
165        // additional invariants
166        unsafe { std::mem::transmute(slice) }
167    }
168
169    /// Gets the underlying byte view of the `LxStr` slice.
170    pub fn as_bytes(&self) -> &[u8] {
171        &self.bytes
172    }
173
174    /// Copies the slice into an owned `LxString`.
175    pub fn to_lx_string(&self) -> LxString {
176        LxString::from_vec(self.bytes.into())
177    }
178
179    /// Returns the length of this `LxStr`.
180    pub fn len(&self) -> usize {
181        self.bytes.len()
182    }
183
184    /// Checks whether the `LxStr` is empty.
185    pub fn is_empty(&self) -> bool {
186        self.bytes.is_empty()
187    }
188
189    /// Yields a `&str` slice if the `LxStr` is valid Unicode.
190    pub fn to_str(&self) -> Option<&str> {
191        str::from_utf8(&self.bytes).ok()
192    }
193
194    /// Convert an `LxStr` to a `Cow<str>`.
195    ///
196    /// Any non-Unicode sequences are replaced with `U+FFFD REPLACEMENT CHARACTER`.
197    pub fn to_string_lossy(&self) -> borrow::Cow<'_, str> {
198        String::from_utf8_lossy(&self.bytes)
199    }
200}
201
202impl ToOwned for LxStr {
203    type Owned = LxString;
204
205    fn to_owned(&self) -> Self::Owned {
206        self.to_lx_string()
207    }
208}
209
210impl AsRef<LxStr> for LxStr {
211    fn as_ref(&self) -> &LxStr {
212        self
213    }
214}
215
216impl AsRef<LxStr> for LxString {
217    fn as_ref(&self) -> &LxStr {
218        self
219    }
220}
221
222impl AsRef<LxStr> for str {
223    fn as_ref(&self) -> &LxStr {
224        LxStr::from_bytes(self.as_bytes())
225    }
226}
227
228impl AsRef<LxStr> for String {
229    fn as_ref(&self) -> &LxStr {
230        (**self).as_ref()
231    }
232}
233
234impl PartialEq<LxStr> for str {
235    fn eq(&self, other: &LxStr) -> bool {
236        LxStr::new(self).eq(other)
237    }
238}
239
240impl PartialEq<str> for LxStr {
241    fn eq(&self, other: &str) -> bool {
242        self.eq(LxStr::new(other))
243    }
244}
245
246impl fmt::Debug for LxStr {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        // This isn't quite identical to how Debug works on OsStr, but that requires the use of
249        // Utf8Lossy which is not stable.
250        let value = self.to_string_lossy();
251        f.write_str("\"")?;
252        for c in value.chars().flat_map(|c| c.escape_debug()) {
253            f.write_char(c)?
254        }
255
256        f.write_str("\"")
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn lx_string_new() {
266        let s = LxString::new();
267        assert_eq!(s.capacity(), 0);
268        assert_eq!(s.len(), 0);
269        assert!(s.is_empty());
270        assert_eq!(s, "");
271        assert_ne!(s, "something");
272    }
273
274    #[test]
275    fn lx_string_capacity() {
276        let s = LxString::with_capacity(100);
277        assert_eq!(s.capacity(), 100);
278        assert_eq!(s.len(), 0);
279        assert!(s.is_empty());
280        assert_eq!(s, "");
281        assert_ne!(s, "something");
282    }
283
284    #[test]
285    fn lx_string_from() {
286        let s = LxString::from("foo");
287        assert_eq!(s.capacity(), 3);
288        assert_eq!(s.len(), 3);
289        assert!(!s.is_empty());
290        assert_eq!(s, "foo");
291        assert_ne!(s, "something");
292        assert_ne!(s, "");
293
294        let s = LxString::from(String::from("foo"));
295        assert_eq!(s.capacity(), 3);
296        assert_eq!(s.len(), 3);
297        assert!(!s.is_empty());
298        assert_eq!(s, "foo");
299        assert_ne!(s, "something");
300        assert_ne!(s, "");
301    }
302
303    #[test]
304    fn lx_string_from_vec() {
305        let s = LxString::from_vec(b"foo".to_vec());
306        assert_eq!(s.capacity(), 3);
307        assert_eq!(s.len(), 3);
308        assert!(!s.is_empty());
309        assert_eq!(s, "foo");
310        assert_ne!(s, "something");
311        assert_ne!(s, "");
312        let vec = s.into_vec();
313        assert_eq!(vec, b"foo");
314    }
315
316    #[test]
317    fn lx_string_into_string() {
318        let s = LxString::from("foo");
319        let s = s.into_string().unwrap();
320        assert_eq!(s, "foo");
321
322        let s = LxString::from_vec(vec![b'a', 0xfe, 0xfe]);
323        let e = s.into_string().unwrap_err();
324        assert_eq!(e, LxString::from_vec(vec![b'a', 0xfe, 0xfe]));
325    }
326
327    #[test]
328    fn lx_string_debug() {
329        let s = LxString::from("foo\\bar");
330        let debug = format!("{:?}", s);
331        assert_eq!(debug, r#""foo\\bar""#)
332    }
333
334    #[test]
335    fn lx_str_new() {
336        let s = LxStr::new("");
337        assert_eq!(s.len(), 0);
338        assert!(s.is_empty());
339        assert_eq!(s, "");
340        assert_ne!(s, "something");
341
342        let s = LxStr::new("foo");
343        assert_eq!(s.len(), 3);
344        assert!(!s.is_empty());
345        assert_eq!(s, "foo");
346        assert_eq!(s, LxStr::new("foo"));
347        assert_ne!(s, "something");
348        assert_ne!(s, "");
349    }
350
351    #[test]
352    fn lx_str_from_bytes() {
353        let s = LxStr::from_bytes(b"foo");
354        assert_eq!(s.len(), 3);
355        assert!(!s.is_empty());
356        assert_eq!(s, "foo");
357        assert_ne!(s, "something");
358        assert_ne!(s, "");
359    }
360
361    #[test]
362    fn lx_str_from_lx_string() {
363        let s = LxString::from("foo");
364        let s = s.as_lx_str();
365        assert_eq!(s.len(), 3);
366        assert!(!s.is_empty());
367        assert_eq!(s, "foo");
368        assert_ne!(s, "something");
369        assert_ne!(s, "");
370    }
371
372    #[test]
373    fn lx_str_to_str() {
374        let s = LxStr::new("foo");
375        let s = s.to_str().unwrap();
376        assert_eq!(s, "foo");
377
378        let s = LxStr::from_bytes(&[b'a', 0xfe, 0xfe]);
379        assert!(s.to_str().is_none());
380    }
381
382    #[test]
383    fn lx_str_to_string_lossy() {
384        let s = LxStr::new("foo");
385        let s = s.to_string_lossy();
386        assert!(matches!(s, borrow::Cow::Borrowed(_)));
387        assert_eq!(s, "foo");
388        let s = LxStr::from_bytes(&[b'a', 0xfe, 0xfe, b'b']);
389
390        let s = s.to_string_lossy();
391        assert!(matches!(s, borrow::Cow::Owned(_)));
392        assert_eq!(s, "a\u{fffd}\u{fffd}b");
393    }
394
395    #[test]
396    fn lx_str_debug() {
397        let s = LxStr::new("foo\\bar");
398        let debug = format!("{:?}", s);
399        assert_eq!(debug, r#""foo\\bar""#)
400    }
401}