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
147impl FromIterator<char> for LxString {
148    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
149        String::from_iter(iter).into()
150    }
151}
152
153/// A borrowed reference to a string that may or may not be valid utf-8.
154///
155/// This is analogous to `OsStr` on Linux, but behaves the same on all platforms.
156#[derive(PartialEq, Eq, Hash)]
157#[repr(transparent)]
158pub struct LxStr {
159    bytes: [u8],
160}
161
162impl LxStr {
163    /// Coerces into an `LxStr` slice.
164    pub fn new<T: AsRef<LxStr> + ?Sized>(s: &T) -> &LxStr {
165        s.as_ref()
166    }
167
168    /// Creates an `LxStr` from a byte slice.
169    pub fn from_bytes(slice: &[u8]) -> &LxStr {
170        // SAFETY: &LxStr has the same repr as &[u8], and doesn't add any
171        // additional invariants
172        unsafe { std::mem::transmute(slice) }
173    }
174
175    /// Gets the underlying byte view of the `LxStr` slice.
176    pub fn as_bytes(&self) -> &[u8] {
177        &self.bytes
178    }
179
180    /// Copies the slice into an owned `LxString`.
181    pub fn to_lx_string(&self) -> LxString {
182        LxString::from_vec(self.bytes.into())
183    }
184
185    /// Returns the length of this `LxStr`.
186    pub fn len(&self) -> usize {
187        self.bytes.len()
188    }
189
190    /// Checks whether the `LxStr` is empty.
191    pub fn is_empty(&self) -> bool {
192        self.bytes.is_empty()
193    }
194
195    /// Yields a `&str` slice if the `LxStr` is valid Unicode.
196    pub fn to_str(&self) -> Option<&str> {
197        str::from_utf8(&self.bytes).ok()
198    }
199
200    /// Convert an `LxStr` to a `Cow<str>`.
201    ///
202    /// Any non-Unicode sequences are replaced with `U+FFFD REPLACEMENT CHARACTER`.
203    pub fn to_string_lossy(&self) -> borrow::Cow<'_, str> {
204        String::from_utf8_lossy(&self.bytes)
205    }
206}
207
208impl ToOwned for LxStr {
209    type Owned = LxString;
210
211    fn to_owned(&self) -> Self::Owned {
212        self.to_lx_string()
213    }
214}
215
216impl AsRef<LxStr> for LxStr {
217    fn as_ref(&self) -> &LxStr {
218        self
219    }
220}
221
222impl AsRef<LxStr> for LxString {
223    fn as_ref(&self) -> &LxStr {
224        self
225    }
226}
227
228impl AsRef<LxStr> for str {
229    fn as_ref(&self) -> &LxStr {
230        LxStr::from_bytes(self.as_bytes())
231    }
232}
233
234impl AsRef<LxStr> for String {
235    fn as_ref(&self) -> &LxStr {
236        (**self).as_ref()
237    }
238}
239
240impl PartialEq<LxStr> for str {
241    fn eq(&self, other: &LxStr) -> bool {
242        LxStr::new(self).eq(other)
243    }
244}
245
246impl PartialEq<str> for LxStr {
247    fn eq(&self, other: &str) -> bool {
248        self.eq(LxStr::new(other))
249    }
250}
251
252impl fmt::Debug for LxStr {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        // This isn't quite identical to how Debug works on OsStr, but that requires the use of
255        // Utf8Lossy which is not stable.
256        let value = self.to_string_lossy();
257        f.write_str("\"")?;
258        for c in value.chars().flat_map(|c| c.escape_debug()) {
259            f.write_char(c)?
260        }
261
262        f.write_str("\"")
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn lx_string_new() {
272        let s = LxString::new();
273        assert_eq!(s.capacity(), 0);
274        assert_eq!(s.len(), 0);
275        assert!(s.is_empty());
276        assert_eq!(s, "");
277        assert_ne!(s, "something");
278    }
279
280    #[test]
281    fn lx_string_capacity() {
282        let s = LxString::with_capacity(100);
283        assert_eq!(s.capacity(), 100);
284        assert_eq!(s.len(), 0);
285        assert!(s.is_empty());
286        assert_eq!(s, "");
287        assert_ne!(s, "something");
288    }
289
290    #[test]
291    fn lx_string_from() {
292        let s = LxString::from("foo");
293        assert_eq!(s.capacity(), 3);
294        assert_eq!(s.len(), 3);
295        assert!(!s.is_empty());
296        assert_eq!(s, "foo");
297        assert_ne!(s, "something");
298        assert_ne!(s, "");
299
300        let s = LxString::from(String::from("foo"));
301        assert_eq!(s.capacity(), 3);
302        assert_eq!(s.len(), 3);
303        assert!(!s.is_empty());
304        assert_eq!(s, "foo");
305        assert_ne!(s, "something");
306        assert_ne!(s, "");
307    }
308
309    #[test]
310    fn lx_string_from_vec() {
311        let s = LxString::from_vec(b"foo".to_vec());
312        assert_eq!(s.capacity(), 3);
313        assert_eq!(s.len(), 3);
314        assert!(!s.is_empty());
315        assert_eq!(s, "foo");
316        assert_ne!(s, "something");
317        assert_ne!(s, "");
318        let vec = s.into_vec();
319        assert_eq!(vec, b"foo");
320    }
321
322    #[test]
323    fn lx_string_into_string() {
324        let s = LxString::from("foo");
325        let s = s.into_string().unwrap();
326        assert_eq!(s, "foo");
327
328        let s = LxString::from_vec(vec![b'a', 0xfe, 0xfe]);
329        let e = s.into_string().unwrap_err();
330        assert_eq!(e, LxString::from_vec(vec![b'a', 0xfe, 0xfe]));
331    }
332
333    #[test]
334    fn lx_string_debug() {
335        let s = LxString::from("foo\\bar");
336        let debug = format!("{:?}", s);
337        assert_eq!(debug, r#""foo\\bar""#)
338    }
339
340    #[test]
341    fn lx_str_new() {
342        let s = LxStr::new("");
343        assert_eq!(s.len(), 0);
344        assert!(s.is_empty());
345        assert_eq!(s, "");
346        assert_ne!(s, "something");
347
348        let s = LxStr::new("foo");
349        assert_eq!(s.len(), 3);
350        assert!(!s.is_empty());
351        assert_eq!(s, "foo");
352        assert_eq!(s, LxStr::new("foo"));
353        assert_ne!(s, "something");
354        assert_ne!(s, "");
355    }
356
357    #[test]
358    fn lx_str_from_bytes() {
359        let s = LxStr::from_bytes(b"foo");
360        assert_eq!(s.len(), 3);
361        assert!(!s.is_empty());
362        assert_eq!(s, "foo");
363        assert_ne!(s, "something");
364        assert_ne!(s, "");
365    }
366
367    #[test]
368    fn lx_str_from_lx_string() {
369        let s = LxString::from("foo");
370        let s = s.as_lx_str();
371        assert_eq!(s.len(), 3);
372        assert!(!s.is_empty());
373        assert_eq!(s, "foo");
374        assert_ne!(s, "something");
375        assert_ne!(s, "");
376    }
377
378    #[test]
379    fn lx_str_to_str() {
380        let s = LxStr::new("foo");
381        let s = s.to_str().unwrap();
382        assert_eq!(s, "foo");
383
384        let s = LxStr::from_bytes(&[b'a', 0xfe, 0xfe]);
385        assert!(s.to_str().is_none());
386    }
387
388    #[test]
389    fn lx_str_to_string_lossy() {
390        let s = LxStr::new("foo");
391        let s = s.to_string_lossy();
392        assert!(matches!(s, borrow::Cow::Borrowed(_)));
393        assert_eq!(s, "foo");
394        let s = LxStr::from_bytes(&[b'a', 0xfe, 0xfe, b'b']);
395
396        let s = s.to_string_lossy();
397        assert!(matches!(s, borrow::Cow::Owned(_)));
398        assert_eq!(s, "a\u{fffd}\u{fffd}b");
399    }
400
401    #[test]
402    fn lx_str_debug() {
403        let s = LxStr::new("foo\\bar");
404        let debug = format!("{:?}", s);
405        assert_eq!(debug, r#""foo\\bar""#)
406    }
407}