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