rusl/string/
unix_str.rs

1use crate::Error;
2#[cfg(feature = "alloc")]
3use alloc::string::String;
4#[cfg(feature = "alloc")]
5use alloc::vec::Vec;
6use core::fmt::{Debug, Formatter};
7use core::hash::Hasher;
8
9use crate::platform::NULL_BYTE;
10use crate::string::strlen::strlen;
11
12#[cfg(feature = "alloc")]
13#[repr(transparent)]
14#[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
15pub struct UnixString(pub(crate) Vec<u8>);
16
17#[cfg(feature = "alloc")]
18impl Debug for UnixString {
19    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
20        let slice = unsafe { core::slice::from_raw_parts(self.0.as_ptr(), self.0.len()) };
21        match core::str::from_utf8(slice) {
22            Ok(raw) => f.write_fmt(format_args!("UnixString({raw})")),
23            Err(_e) => f.write_fmt(format_args!("UnixString({slice:?})")),
24        }
25    }
26}
27
28#[cfg(feature = "alloc")]
29impl UnixString {
30    #[inline]
31    #[must_use]
32    pub fn as_ptr(&self) -> *const u8 {
33        self.0.as_ptr()
34    }
35
36    /// Create a `UnixString` from a `String`.
37    /// # Errors
38    /// String has nulls in other places than end.
39    #[inline]
40    pub fn try_from_string(s: String) -> Result<Self, Error> {
41        Self::try_from_vec(s.into_bytes())
42    }
43
44    /// Create a `UnixString` from a `Vec<u8>`.
45    /// # Errors
46    /// Vec has nulls in other places than end.
47    #[inline]
48    pub fn try_from_vec(mut s: Vec<u8>) -> Result<Self, Error> {
49        let len = s.len();
50        for (ind, byte) in s.iter().enumerate() {
51            if *byte == NULL_BYTE {
52                return if ind == len - 1 {
53                    unsafe { Ok(core::mem::transmute::<Vec<u8>, Self>(s)) }
54                } else {
55                    Err(Error::no_code("Tried to instantiate UnixStr from an invalid &str, a null byte was found but out of place"))
56                };
57            }
58        }
59        s.push(0);
60        Ok(Self(s))
61    }
62
63    /// Create a `UnixString` from a `&[u8]`.
64    /// Will allocate and push a null byte if not null terminated
65    /// # Errors
66    /// Bytes aren't properly null terminated, several nulls contained.
67    pub fn try_from_bytes(s: &[u8]) -> Result<Self, Error> {
68        let len = s.len();
69        for (ind, byte) in s.iter().enumerate() {
70            if *byte == NULL_BYTE {
71                return if ind == len - 1 {
72                    unsafe { Ok(core::mem::transmute::<Vec<u8>, Self>(s.to_vec())) }
73                } else {
74                    Err(Error::no_code("Tried to instantiate UnixStr from an invalid &str, a null byte was found but out of place"))
75                };
76            }
77        }
78        let mut new = s.to_vec();
79        new.push(0);
80        Ok(Self(new))
81    }
82
83    /// Create a `UnixString` from a `&str`.
84    /// Will allocate and push a null byte if not null terminated
85    /// # Errors
86    /// String isn't properly null terminated, several nulls contained.
87    #[inline]
88    pub fn try_from_str(s: &str) -> Result<Self, Error> {
89        Self::try_from_bytes(s.as_bytes())
90    }
91
92    /// A slightly less efficient that creating a format string way of
93    /// creating a [`UnixString`], since it'll in all likelihood lead to two allocations.
94    /// Can't do much since fmt internals are feature gated in `core`.
95    /// Still a bit more ergonomic than creating a format-String and then creating a [`UnixString`] from that.
96    /// More efficient if a null byte is added to the format strings.
97    /// # Example
98    /// ```
99    /// use rusl::string::unix_str::UnixString;
100    /// fn create_format_unix_string() {
101    ///     let ins_with = "gramar";
102    ///     let good = UnixString::from_format(format_args!("/home/{ins_with}/code"));
103    ///     assert_eq!("/home/gramar/code", good.as_str().unwrap());
104    ///     let great = UnixString::from_format(format_args!("/home/{ins_with}/code\0"));
105    ///     assert_eq!("/home/gramar/code", great.as_str().unwrap());
106    /// }
107    /// ```
108    #[must_use]
109    pub fn from_format(args: core::fmt::Arguments<'_>) -> Self {
110        let mut fmt_str_buf = alloc::fmt::format(args).into_bytes();
111        if !matches!(fmt_str_buf.last(), Some(&NULL_BYTE)) {
112            fmt_str_buf.push(NULL_BYTE);
113        }
114        UnixString(fmt_str_buf)
115    }
116}
117
118#[cfg(feature = "alloc")]
119impl core::ops::Deref for UnixString {
120    type Target = UnixStr;
121
122    fn deref(&self) -> &Self::Target {
123        unsafe { &*(core::ptr::from_ref::<[u8]>(self.0.as_slice()) as *const UnixStr) }
124    }
125}
126
127#[cfg(feature = "alloc")]
128impl AsRef<UnixStr> for UnixString {
129    #[inline]
130    fn as_ref(&self) -> &UnixStr {
131        unsafe { UnixStr::from_bytes_unchecked(self.0.as_slice()) }
132    }
133}
134
135#[repr(transparent)]
136#[derive(Eq, PartialEq, Ord, PartialOrd)]
137pub struct UnixStr(pub(crate) [u8]);
138
139impl UnixStr {
140    pub const EMPTY: &'static Self = UnixStr::from_str_checked("\0");
141
142    /// # Safety
143    /// `&str` needs to be null terminated or downstream UB may occur
144    #[inline]
145    #[must_use]
146    pub const unsafe fn from_str_unchecked(s: &str) -> &Self {
147        core::mem::transmute(s)
148    }
149
150    /// # Safety
151    /// `&[u8]` needs to be null terminated or downstream UB may occur
152    #[inline]
153    #[must_use]
154    pub const unsafe fn from_bytes_unchecked(s: &[u8]) -> &Self {
155        core::mem::transmute(s)
156    }
157
158    /// Const instantiation of a `&UnixStr` from a `&str`.
159    /// Should only be used in a `const`-context, although `rustc` does not let me enforce this.
160    /// # Panics
161    /// This method panics since it's supposed to produce a comptime error, it's
162    /// not particularly efficient.
163    #[must_use]
164    pub const fn from_str_checked(s: &str) -> &Self {
165        const_null_term_validate(s.as_bytes());
166        unsafe { core::mem::transmute(s) }
167    }
168
169    /// Create a `&UnixStr` from a `&str`.
170    /// # Errors
171    /// String isn't properly null terminated.
172    #[inline]
173    pub fn try_from_str(s: &str) -> Result<&Self, Error> {
174        Self::try_from_bytes(s.as_bytes())
175    }
176
177    /// Create a `&UnixStr` from a `&[u8]`.
178    /// # Errors
179    /// Slice isn't properly null terminated.
180    #[inline]
181    pub fn try_from_bytes(s: &[u8]) -> Result<&Self, Error> {
182        let len = s.len();
183        for (ind, byte) in s.iter().enumerate() {
184            if *byte == NULL_BYTE {
185                return if ind == len - 1 {
186                    unsafe { Ok(&*(core::ptr::from_ref::<[u8]>(s) as *const UnixStr)) }
187                } else {
188                    Err(Error::no_code("Tried to instantiate UnixStr from an invalid &str, a null byte was found but out of place"))
189                };
190            }
191        }
192        Err(Error::no_code(
193            "Tried to instantiate UnixStr from an invalid &str, not null terminated",
194        ))
195    }
196
197    /// # Safety
198    /// `s` needs to be null terminated
199    #[must_use]
200    pub unsafe fn from_ptr<'a>(s: *const u8) -> &'a Self {
201        let non_null_len = strlen(s);
202        let slice = core::slice::from_raw_parts(s, non_null_len + 1);
203        &*(core::ptr::from_ref::<[u8]>(slice) as *const Self)
204    }
205
206    /// Try to convert this `&UnixStr` to a utf8 `&str`
207    /// # Errors
208    /// Not utf8
209    pub fn as_str(&self) -> Result<&str, Error> {
210        let slice = unsafe { core::slice::from_raw_parts(self.0.as_ptr(), self.0.len() - 1) };
211        Ok(core::str::from_utf8(slice)?)
212    }
213
214    /// Get this `&UnixStr` as a slice, including the null byte
215    #[inline]
216    #[must_use]
217    pub const fn as_slice(&self) -> &[u8] {
218        &self.0
219    }
220
221    /// Get the length of this `&UnixStr`, including the null byte
222    #[inline]
223    #[must_use]
224    #[expect(clippy::len_without_is_empty)]
225    pub fn len(&self) -> usize {
226        self.0.len()
227    }
228
229    /// Get the length of this `&UnixStr`, including the null byte
230    #[must_use]
231    #[inline]
232    pub fn as_ptr(&self) -> *const u8 {
233        self.0.as_ptr()
234    }
235
236    #[must_use]
237    pub fn match_up_to(&self, other: &UnixStr) -> usize {
238        let mut it = 0;
239        let slf_ptr = self.as_ptr();
240        let other_ptr = other.as_ptr();
241        loop {
242            unsafe {
243                let a_val = slf_ptr.add(it).read();
244                let b_val = other_ptr.add(it).read();
245                if a_val != b_val || a_val == NULL_BYTE {
246                    // Not equal, or terminated
247                    return it;
248                }
249                // Equal continue
250                it += 1;
251            }
252        }
253    }
254
255    #[must_use]
256    pub fn match_up_to_str(&self, other: &str) -> usize {
257        let mut it = 0;
258        let slf_ptr = self.as_ptr();
259        let other_ptr = other.as_ptr();
260        let other_len = other.len();
261        loop {
262            unsafe {
263                let a_val = slf_ptr.add(it).read();
264                let b_val = other_ptr.add(it).read();
265                if a_val != b_val || a_val == NULL_BYTE {
266                    // Not equal, or terminated
267                    return it;
268                }
269                // Equal continue
270                it += 1;
271            }
272            if it == other_len {
273                return it;
274            }
275        }
276    }
277
278    #[must_use]
279    pub fn find(&self, other: &Self) -> Option<usize> {
280        if other.len() > self.len() {
281            return None;
282        }
283        let this_buf = &self.0;
284        let other_buf = &other.0[..other.0.len() - 2];
285        buf_find(this_buf, other_buf)
286    }
287
288    #[must_use]
289    pub fn find_buf(&self, other: &[u8]) -> Option<usize> {
290        if other.len() > self.len() {
291            return None;
292        }
293        let this_buf = &self.0;
294        buf_find(this_buf, other)
295    }
296
297    #[must_use]
298    pub fn ends_with(&self, other: &Self) -> bool {
299        if other.len() > self.len() {
300            return false;
301        }
302        let mut ind = 0;
303        while let (Some(this), Some(that)) = (
304            self.0.get(self.0.len() - 1 - ind),
305            other.0.get(other.0.len() - 1 - ind),
306        ) {
307            if this != that {
308                return false;
309            }
310            if other.0.len() - 1 - ind == 0 {
311                return true;
312            }
313            ind += 1;
314        }
315        true
316    }
317
318    /// Get the last component of a path, if possible.
319    ///
320    /// # Example
321    /// ```
322    /// use rusl::string::unix_str::UnixStr;
323    /// use rusl::unix_lit;
324    /// fn get_file_paths() {
325    ///     // Has no filename, just a root path
326    ///     let a = unix_lit!("/");
327    ///     assert_eq!(None, a.path_file_name());
328    ///     // Has a 'filename'
329    ///     let a = unix_lit!("/etc");
330    ///     assert_eq!(Some(unix_lit!("etc")), a.path_file_name());
331    /// }
332    /// ```
333    #[must_use]
334    pub fn path_file_name(&self) -> Option<&UnixStr> {
335        for (ind, byte) in self.0.iter().enumerate().rev() {
336            if *byte == b'/' {
337                return if ind + 2 < self.len() {
338                    unsafe {
339                        Some(&*(core::ptr::from_ref::<[u8]>(&self.0[ind + 1..]) as *const Self))
340                    }
341                } else {
342                    None
343                };
344            }
345        }
346        None
347    }
348
349    /// Joins this [`UnixStr`] with some other [`UnixStr`] adding a slash if necessary.
350    /// Will make sure that there's at most one slash at the boundary but won't check
351    /// either string for "path validity" in any other case
352    /// # Example
353    /// ```
354    /// use rusl::string::unix_str::UnixStr;
355    /// fn join_paths() {
356    ///     // Combines slash
357    ///     let a = UnixStr::try_from_str("path/").unwrap();
358    ///     let b = UnixStr::try_from_str("/ext").unwrap();
359    ///     let combined = a.path_join(b);
360    ///     assert_eq!("path/ext", combined.as_str().unwrap());
361    ///     // Adds slash
362    ///     let combined_other_way = b.path_join(a);
363    ///     assert_eq!("/ext/path/", combined_other_way.as_str().unwrap());
364    ///     // Doesn't truncate other slashes, only works at the boundary between the two paths
365    ///     let a = UnixStr::try_from_str("path//").unwrap();
366    ///     let combined_many_slashes = a.path_join(b);
367    ///     assert_eq!("path//ext", combined_many_slashes.as_str().unwrap());
368    /// }
369    /// ```
370    #[must_use]
371    #[cfg(feature = "alloc")]
372    pub fn path_join(&self, ext: &Self) -> UnixString {
373        let mut as_string = self.0.to_vec();
374        as_string.pop();
375        let Some(last) = as_string.last().copied() else {
376            return UnixString::from(ext);
377        };
378        if ext.len() == 1 {
379            return UnixString::from(self);
380        }
381        as_string.reserve(ext.len());
382        let buf = if last == b'/' {
383            unsafe {
384                if ext.0.get_unchecked(0) == &b'/' {
385                    as_string.extend_from_slice(ext.0.get_unchecked(1..));
386                } else {
387                    as_string.extend_from_slice(&ext.0);
388                }
389            }
390            as_string
391        } else if unsafe { ext.0.get_unchecked(0) == &b'/' } {
392            as_string.extend_from_slice(&ext.0);
393            as_string
394        } else {
395            as_string.push(b'/');
396            as_string.extend_from_slice(&ext.0);
397            as_string
398        };
399        UnixString(buf)
400    }
401
402    /// Joins this [`UnixStr`] with some format string adding a slash if necessary.
403    /// Follows the same rules as [`UnixStr::path_join`].
404    /// # Example
405    /// ```
406    /// use rusl::string::unix_str::UnixStr;
407    /// fn join_paths() {
408    ///     // Combines slash
409    ///     let a = UnixStr::try_from_str("path/").unwrap();
410    ///     let combined = a.path_join_fmt(format_args!("ext"));
411    ///     assert_eq!("path/ext", combined.as_str().unwrap());
412    /// }
413    /// ```
414    #[must_use]
415    #[cfg(feature = "alloc")]
416    pub fn path_join_fmt(&self, args: core::fmt::Arguments<'_>) -> UnixString {
417        let container = alloc::fmt::format(args);
418        if container.is_empty() {
419            return UnixString::from(self);
420        }
421        let mut container_vec = container.into_bytes();
422        let mut as_string = self.0.to_vec();
423        as_string.pop();
424        let Some(last) = as_string.last().copied() else {
425            if !matches!(container_vec.last().copied(), Some(NULL_BYTE)) {
426                container_vec.push(NULL_BYTE);
427            }
428            return UnixString(container_vec);
429        };
430        if last == b'/' {
431            as_string.reserve(container_vec.len() + 1);
432            let start_from = if let Some(b'/') = container_vec.first().copied() {
433                1
434            } else {
435                0
436            };
437            if let Some(add_slice) = container_vec.get(start_from..) {
438                as_string.extend_from_slice(add_slice);
439            }
440            if !matches!(as_string.last().copied(), Some(NULL_BYTE)) {
441                as_string.push(NULL_BYTE);
442            }
443        } else if let Some(b'/') = container_vec.first().copied() {
444            as_string.extend(container_vec);
445            if !matches!(as_string.last().copied(), Some(NULL_BYTE)) {
446                as_string.push(NULL_BYTE);
447            }
448        } else {
449            as_string.push(b'/');
450            as_string.extend(container_vec);
451            if !matches!(as_string.last().copied(), Some(NULL_BYTE)) {
452                as_string.push(NULL_BYTE);
453            }
454        }
455        UnixString(as_string)
456    }
457
458    /// Treats this [`UnixStr`] as a path, then tries to find its parent.
459    /// Will treat any double slash as a path with no parent
460    /// # Example
461    /// ```
462    /// use rusl::string::unix_str::UnixStr;
463    /// fn find_parent() {
464    ///     let well_formed = UnixStr::try_from_str("/home/gramar/code/").unwrap();
465    ///     let up_one = well_formed.parent_path().unwrap();
466    ///     assert_eq!("/home/gramar", up_one.as_str().unwrap());
467    ///     let up_two = up_one.parent_path().unwrap();
468    ///     assert_eq!("/home", up_two.as_str().unwrap());
469    ///     let up_three = up_two.parent_path().unwrap();
470    ///     assert_eq!("/", up_three.as_str().unwrap());
471    ///     assert!(up_three.parent_path().is_none());
472    ///     let ill_formed = UnixStr::try_from_str("/home/gramar/code//").unwrap();
473    ///     assert!(ill_formed.parent_path().is_none());
474    /// }
475    /// ```
476    #[must_use]
477    #[cfg(feature = "alloc")]
478    pub fn parent_path(&self) -> Option<UnixString> {
479        let len = self.0.len();
480        // Can't be len 0, len 1 is only a null byte, len 2 is a single char, parent becomes none
481        if len < 3 {
482            return None;
483        }
484        let last = self.0.len() - 2;
485        let mut next_slash_back = last;
486        while let Some(byte) = self.0.get(next_slash_back).copied() {
487            if byte == b'/' {
488                if next_slash_back != 0 {
489                    if let Some(b'/') = self.0.get(next_slash_back - 1) {
490                        return None;
491                    }
492                }
493                break;
494            }
495            if next_slash_back == 0 {
496                return None;
497            }
498            next_slash_back -= 1;
499        }
500        // Found slash at root, we want to include it in output then giving only the root path
501        if next_slash_back == 0 {
502            next_slash_back += 1;
503        }
504        unsafe {
505            Some(UnixString(
506                self.0.get_unchecked(..=next_slash_back).to_vec(),
507            ))
508        }
509    }
510}
511
512#[inline]
513#[expect(clippy::needless_range_loop)]
514fn buf_find(this_buf: &[u8], other_buf: &[u8]) -> Option<usize> {
515    for i in 0..this_buf.len() {
516        if this_buf[i] == other_buf[0] {
517            let mut no_match = false;
518            for j in 1..other_buf.len() {
519                if let Some(this) = this_buf.get(i + j) {
520                    if *this != other_buf[j] {
521                        no_match = true;
522                        break;
523                    }
524                } else {
525                    return None;
526                }
527            }
528            if !no_match {
529                return Some(i);
530            }
531        }
532    }
533    None
534}
535
536impl Debug for &UnixStr {
537    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
538        let slice = unsafe { core::slice::from_raw_parts(self.0.as_ptr(), self.0.len()) };
539        match core::str::from_utf8(slice) {
540            Ok(inner) => f.write_fmt(format_args!("UnixStr({inner})")),
541            Err(_e) => f.write_fmt(format_args!("UnixStr({slice:?})")),
542        }
543    }
544}
545
546impl core::hash::Hash for &UnixStr {
547    #[inline]
548    fn hash<H: Hasher>(&self, state: &mut H) {
549        self.0.hash(state);
550    }
551}
552
553#[cfg(feature = "alloc")]
554impl From<&UnixStr> for UnixString {
555    #[inline]
556    fn from(s: &UnixStr) -> Self {
557        UnixString(s.0.to_vec())
558    }
559}
560
561#[cfg(feature = "alloc")]
562impl core::str::FromStr for UnixString {
563    type Err = Error;
564
565    #[inline]
566    fn from_str(s: &str) -> Result<Self, Self::Err> {
567        Self::try_from_str(s)
568    }
569}
570
571#[inline]
572const fn const_null_term_validate(s: &[u8]) {
573    assert!(
574        !s.is_empty(),
575        "Tried to instantiate UnixStr from an invalid &str, not null terminated"
576    );
577    let len = s.len() - 1;
578    let mut i = len;
579    assert!(
580        s[i] == b'\0',
581        "Tried to instantiate UnixStr from an invalid &str, not null terminated"
582    );
583    while i > 0 {
584        i -= 1;
585        assert!(s[i] != b'\0', "Tried to instantiate UnixStr from an invalid &str, a null byte was found but out of place");
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    #[test]
593    fn can_match_up_to() {
594        let haystack = UnixStr::try_from_str("haystack\0").unwrap();
595        let needle = UnixStr::EMPTY;
596        assert_eq!(0, haystack.match_up_to(needle));
597        let needle = UnixStr::try_from_str("h\0").unwrap();
598        assert_eq!(1, haystack.match_up_to(needle));
599        let needle = UnixStr::try_from_str("haystac\0").unwrap();
600        assert_eq!(7, haystack.match_up_to(needle));
601        let needle = UnixStr::try_from_str("haystack\0").unwrap();
602        assert_eq!(8, haystack.match_up_to(needle));
603        let needle = UnixStr::try_from_str("haystack2\0").unwrap();
604        assert_eq!(8, haystack.match_up_to(needle));
605    }
606
607    #[test]
608    fn can_create_unix_str() {
609        const CONST_CORRECT: &UnixStr = UnixStr::from_str_checked("abc\0");
610
611        let correct1 = UnixStr::try_from_str("abc\0").unwrap();
612        let correct2 = UnixStr::try_from_bytes(b"abc\0").unwrap();
613        assert_eq!(CONST_CORRECT, correct1);
614        assert_eq!(correct1, correct2);
615    }
616
617    #[test]
618    fn can_create_unix_str_sad() {
619        let unacceptable = UnixStr::try_from_str("a\0bc");
620        assert!(unacceptable.is_err());
621        let unacceptable_vec = UnixStr::try_from_bytes(b"a\0bc");
622        assert!(unacceptable_vec.is_err());
623        let unacceptable_not_null_term = UnixStr::try_from_str("abc");
624        assert!(unacceptable_not_null_term.is_err());
625    }
626
627    #[test]
628    fn can_cmp_unix_str_and_str() {
629        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
630        const MY_CMP_STR: &str = "my-nice-str";
631        assert_eq!(MY_CMP_STR.len(), UNIX_STR.match_up_to_str(MY_CMP_STR));
632    }
633
634    #[test]
635    fn can_check_ends_with() {
636        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
637        assert!(UNIX_STR.ends_with(UnixStr::from_str_checked("-str\0")));
638    }
639
640    #[test]
641    fn can_check_ends_with_self() {
642        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
643        assert!(UNIX_STR.ends_with(UNIX_STR));
644    }
645
646    #[test]
647    fn can_check_ends_with_empty() {
648        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
649        assert!(UNIX_STR.ends_with(UnixStr::EMPTY));
650    }
651
652    #[test]
653    fn can_check_ends_with_no() {
654        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
655        assert!(!UNIX_STR.ends_with(UnixStr::from_str_checked("nice-\0")));
656    }
657
658    #[test]
659    fn can_check_ends_with_no_too_long() {
660        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
661        assert!(!UNIX_STR.ends_with(UnixStr::from_str_checked("other-my-nice-str\0")));
662    }
663
664    #[test]
665    fn can_find_at_end() {
666        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
667        let found_at = UNIX_STR.find(UnixStr::from_str_checked("-str\0")).unwrap();
668        assert_eq!(7, found_at);
669    }
670
671    #[test]
672    fn can_find_finds_first() {
673        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("str-str-str\0");
674        let found_at = UNIX_STR.find(UnixStr::from_str_checked("-str\0")).unwrap();
675        assert_eq!(3, found_at);
676        let found_at = UNIX_STR.find_buf("-str".as_bytes()).unwrap();
677        assert_eq!(3, found_at);
678    }
679
680    #[test]
681    fn can_find_at_middle() {
682        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
683        let found_at = UNIX_STR.find(UnixStr::from_str_checked("-nice\0")).unwrap();
684        assert_eq!(2, found_at);
685        let found_at = UNIX_STR.find_buf("-nice".as_bytes()).unwrap();
686        assert_eq!(2, found_at);
687    }
688
689    #[test]
690    fn can_find_at_start() {
691        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
692        let found_at = UNIX_STR.find(UnixStr::from_str_checked("my\0")).unwrap();
693        assert_eq!(0, found_at);
694        let found_at = UNIX_STR.find_buf("my".as_bytes()).unwrap();
695        assert_eq!(0, found_at);
696    }
697
698    #[test]
699    fn can_find_no_match() {
700        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("my-nice-str\0");
701        let found_at = UNIX_STR.find(UnixStr::from_str_checked("cake\0"));
702        assert!(found_at.is_none());
703        let found_at = UNIX_STR.find_buf("cake".as_bytes());
704        assert!(found_at.is_none());
705    }
706
707    #[test]
708    fn can_find_too_long() {
709        const UNIX_STR: &UnixStr = UnixStr::from_str_checked("str\0");
710        let found_at = UNIX_STR.find(UnixStr::from_str_checked("sstr\0"));
711        assert!(found_at.is_none());
712
713        let found_at = UNIX_STR.find_buf("sstr".as_bytes());
714        assert!(found_at.is_none());
715    }
716
717    #[cfg(feature = "alloc")]
718    mod alloc_tests {
719        use super::*;
720        use alloc::string::ToString;
721        #[test]
722        #[cfg(feature = "alloc")]
723        fn can_create_unix_string_sad() {
724            let acceptable = UnixString::try_from_str("abc").unwrap();
725            let correct = UnixString::try_from_str("abc\0").unwrap();
726            assert_eq!(correct, acceptable);
727            let unacceptable = UnixString::try_from_str("a\0bc");
728            assert!(unacceptable.is_err());
729            let unacceptable_vec = UnixString::try_from_vec(alloc::vec![b'a', b'\0', b'b', b'c']);
730            assert!(unacceptable_vec.is_err());
731        }
732        #[test]
733        fn can_path_join() {
734            let a = UnixStr::from_str_checked("hello\0");
735            let b = UnixStr::from_str_checked("there\0");
736            let new = a.path_join(b);
737            assert_eq!("hello/there", new.as_str().unwrap());
738        }
739
740        #[test]
741        fn can_path_join_fmt() {
742            let a = UnixStr::from_str_checked("hello\0");
743            let new = a.path_join_fmt(format_args!("there"));
744            assert_eq!("hello/there", new.as_str().unwrap());
745        }
746
747        #[test]
748        fn can_path_join_with_trailing_slash() {
749            let a = UnixStr::from_str_checked("hello/\0");
750            let b = UnixStr::from_str_checked("there\0");
751            let new = a.path_join(b);
752            assert_eq!("hello/there", new.as_str().unwrap());
753        }
754
755        #[test]
756        fn can_path_join_fmt_with_trailing_slash() {
757            let a = UnixStr::from_str_checked("hello/\0");
758            let new = a.path_join_fmt(format_args!("there"));
759            assert_eq!("hello/there", new.as_str().unwrap());
760        }
761        #[test]
762        fn can_path_join_with_leading_slash() {
763            let a = UnixStr::from_str_checked("hello\0");
764            let b = UnixStr::from_str_checked("/there\0");
765            let new = a.path_join(b);
766            assert_eq!("hello/there", new.as_str().unwrap());
767        }
768
769        #[test]
770        fn can_path_join_fmt_with_leading_slash() {
771            let a = UnixStr::from_str_checked("hello\0");
772            let new = a.path_join_fmt(format_args!("/there"));
773            assert_eq!("hello/there", new.as_str().unwrap());
774        }
775
776        #[test]
777        fn can_path_join_empty() {
778            let a = UnixStr::from_str_checked("\0");
779            let b = UnixStr::from_str_checked("/there\0");
780            let new = a.path_join(b);
781            assert_eq!("/there", new.as_str().unwrap());
782            let new = b.path_join(a);
783            assert_eq!("/there", new.as_str().unwrap());
784        }
785
786        #[test]
787        fn can_path_join_fmt_empty() {
788            let a = UnixStr::from_str_checked("\0");
789            let b = UnixStr::from_str_checked("/there\0");
790            let new = a.path_join_fmt(format_args!("/there"));
791            assert_eq!("/there", new.as_str().unwrap());
792            let new = b.path_join_fmt(format_args!(""));
793            assert_eq!("/there", new.as_str().unwrap());
794        }
795
796        #[test]
797        fn can_path_join_truncates_slashes() {
798            let a = UnixStr::from_str_checked("hello/\0");
799            let b = UnixStr::from_str_checked("/there\0");
800            let new = a.path_join(b);
801            assert_eq!("hello/there", new.as_str().unwrap());
802        }
803
804        #[test]
805        fn can_get_last_path_happy() {
806            let base = unix_lit!("a/b/c");
807            let res = base.path_file_name().unwrap();
808            let expect = unix_lit!("c");
809            assert_eq!(expect, res);
810        }
811
812        #[test]
813        fn can_get_last_path_root_gives_none() {
814            let base = unix_lit!("/");
815            assert!(base.path_file_name().is_none());
816        }
817
818        #[test]
819        fn can_get_last_path_empty_gives_none() {
820            assert!(UnixStr::EMPTY.path_file_name().is_none());
821        }
822
823        #[test]
824        fn find_parent_path_happy() {
825            let a = UnixStr::from_str_checked("hello/there/friend\0");
826            let parent = a.parent_path().unwrap();
827            assert_eq!("hello/there", parent.as_str().unwrap());
828            let b = UnixStr::from_str_checked("/home/gramar/code/rust/tiny-std\0");
829            let b_first_parent = b.parent_path().unwrap();
830            assert_eq!("/home/gramar/code/rust", b_first_parent.as_str().unwrap());
831            let b_second_parent = b_first_parent.parent_path().unwrap();
832            assert_eq!("/home/gramar/code", b_second_parent.as_str().unwrap());
833            let b_third_parent = b_second_parent.parent_path().unwrap();
834            assert_eq!("/home/gramar", b_third_parent.as_str().unwrap());
835            let b_fourth_parent = b_third_parent.parent_path().unwrap();
836            assert_eq!("/home", b_fourth_parent.as_str().unwrap());
837            let root = b_fourth_parent.parent_path().unwrap();
838            assert_eq!("/", root.as_str().unwrap());
839        }
840
841        #[test]
842        fn find_parent_path_empty_no_parent() {
843            let a = UnixStr::EMPTY;
844            let parent = a.parent_path();
845            assert!(parent.is_none());
846        }
847
848        #[test]
849        fn find_parent_path_short_no_parent() {
850            let a = UnixStr::from_str_checked("/\0");
851            let parent = a.parent_path();
852            assert!(parent.is_none());
853            let b = UnixStr::from_str_checked("a\0");
854            let parent = b.parent_path();
855            assert!(parent.is_none());
856        }
857
858        #[test]
859        fn find_parent_path_short_has_parent() {
860            let a = UnixStr::from_str_checked("/a\0");
861            let parent = a.parent_path().unwrap();
862            assert_eq!("/", parent.as_str().unwrap());
863        }
864
865        #[test]
866        fn find_parent_path_double_slash_invalid() {
867            let a = UnixStr::from_str_checked("//\0");
868            let parent = a.parent_path();
869            assert!(parent.is_none());
870            let a = UnixStr::from_str_checked("hello//\0");
871            let parent = a.parent_path();
872            assert!(parent.is_none());
873        }
874
875        #[test]
876        fn can_create_unix_string_happy() {
877            let correct = UnixString::try_from_str("abc\0").unwrap();
878            let correct2 = UnixString::try_from_bytes(b"abc\0").unwrap();
879            let correct3 = UnixString::try_from_string("abc\0".to_string()).unwrap();
880            let correct4 = UnixString::try_from_vec(b"abc\0".to_vec()).unwrap();
881            let correct5 = UnixString::try_from_str("abc").unwrap();
882            let correct6 = UnixString::try_from_string(String::from("abc")).unwrap();
883            assert_eq!(correct, correct2);
884            assert_eq!(correct2, correct3);
885            assert_eq!(correct3, correct4);
886            assert_eq!(correct4, correct5);
887            assert_eq!(correct5, correct6);
888            let compare = [b'a', b'b', b'c', 0];
889            assert_eq!(correct4.as_slice(), compare);
890            assert_ne!(correct.as_ptr(), correct2.as_ptr());
891            assert_eq!(UnixStr::try_from_str("abc\0").unwrap(), correct.as_ref());
892            assert_eq!(UnixStr::try_from_str("abc\0").unwrap(), &*correct);
893        }
894    }
895}