Skip to main content

dream_path/
lib.rs

1//! Byte-first normalized virtual resource paths.
2//!
3//! `dream-path` owns the path normalization shared by archive readers, VFS,
4//! resource loading, rendering-side resource lookup, and tooling. The rules are
5//! intentionally the boring OpenMW-style virtual path rules:
6//!
7//! - `\` becomes `/`
8//! - ASCII uppercase letters become lowercase
9//! - repeated separators collapse
10//! - leading separators are discarded
11//! - arbitrary non-UTF-8 bytes are preserved
12//!
13//! The rules are byte-literal: `///` normalizes to an empty byte string, while a
14//! non-leading trailing separator is kept (`foo/` stays `foo/`).
15//!
16//! It does not decode legacy filename encodings, perform Unicode
17//! normalization, case-fold non-ASCII text, interpret host filesystem paths, or
18//! compute archive-format hashes. Those are separate jobs. Mixing them into the
19//! path type would be a small architectural crime, so naturally we avoid doing
20//! that.
21//!
22//! ## Lua API
23//!
24//! Enable the `lua` feature to embed the same byte-preserving normalization API
25//! into an existing [`mlua::Lua`] state. [`lua::create_module`] builds the API
26//! table without registering a global, while [`lua::register_module`] installs it
27//! as the default `dream_path` global.
28//!
29//! The `lua` feature does not choose a Lua backend. Engine/application crates own
30//! that decision and should enable exactly one shared [`mlua`] runtime for the
31//! final dependency graph. `DreamWeave` recommends `LuaJIT` in 5.2 compatibility mode
32//! and does not currently test these bindings against other Lua runtimes. If a
33//! host chooses another backend, it owns that compatibility burden. For standalone
34//! documentation and local smoke tests, the `standalone-lua` feature enables
35//! `lua` plus `mlua`'s `luajit52` and `vendored` features.
36//!
37//! The Lua API treats Lua strings as raw path bytes, preserving invalid UTF-8 and
38//! embedded NUL bytes. It is embed-only: this crate does not provide a `cdylib`
39//! Lua module loader, and hosts that already own a different Lua runtime should
40//! bind the Rust byte API themselves.
41
42use std::{borrow::Borrow, str::Utf8Error};
43
44use bstr::{BStr, BString};
45
46pub use bstr::ByteSlice;
47
48#[cfg(feature = "lua")]
49pub mod lua;
50
51/// A byte-first normalized virtual resource path.
52///
53/// [`NormalizedPath::new`] and the input [`From`] impls apply [`normalize_path`].
54/// The normalized-byte adoption constructors require the caller to provide bytes
55/// that are already normalized. This type is intended for repeated lookups where
56/// normalizing the query every time would allocate and burn cycles for no useful
57/// reason.
58#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub struct NormalizedPath(BString);
60
61impl NormalizedPath {
62    /// Normalize `path` into an owned virtual resource path.
63    #[must_use]
64    pub fn new(path: impl AsRef<[u8]>) -> Self {
65        Self(BString::from(normalize_path(path.as_ref())))
66    }
67
68    /// Build from bytes that are already normalized.
69    ///
70    /// Returns the original bytes on rejection so callers can log, repair, or
71    /// normalize them without cloning first. Use this when avoiding a second
72    /// normalization pass matters and the caller can handle rejection.
73    ///
74    /// # Errors
75    ///
76    /// Returns the original `path` when it does not satisfy
77    /// [`is_normalized_path`].
78    pub fn try_from_normalized_bytes(path: Vec<u8>) -> Result<Self, Vec<u8>> {
79        if is_normalized_path(&path) {
80            Ok(Self(BString::from(path)))
81        } else {
82            Err(path)
83        }
84    }
85
86    /// Build from bytes that are already normalized without checking them.
87    ///
88    /// `path` should satisfy [`is_normalized_path`]. Passing non-normalized bytes
89    /// breaks the logical invariant that every [`NormalizedPath`] contains
90    /// normalized virtual path spelling. That can produce cache misses and
91    /// duplicate keys. It is not memory-unsafe; it is just wrong, which is quite
92    /// bad enough.
93    #[must_use]
94    pub fn from_normalized_bytes_unchecked(path: Vec<u8>) -> Self {
95        debug_assert!(is_normalized_path(&path));
96        Self(BString::from(path))
97    }
98
99    /// Return this path as a [`BStr`].
100    #[must_use]
101    pub fn as_bstr(&self) -> &BStr {
102        self.0.as_bstr()
103    }
104
105    /// Return this path as raw bytes.
106    #[must_use]
107    pub fn as_bytes(&self) -> &[u8] {
108        &self.0
109    }
110
111    /// Return this path as UTF-8 when the normalized bytes are valid UTF-8.
112    ///
113    /// This is a convenience for string-oriented callers. It is not a promise
114    /// that virtual resource paths are Unicode, display-safe, C-string-safe, or
115    /// host filesystem paths. Valid UTF-8 paths may still contain NUL bytes.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`Utf8Error`] if the path contains invalid UTF-8 bytes.
120    pub fn to_str(&self) -> Result<&str, Utf8Error> {
121        std::str::from_utf8(self.as_bytes())
122    }
123
124    /// Return the final non-empty component of this virtual path.
125    ///
126    /// A trailing separator is ignored for component extraction, so `foo/bar/`
127    /// has file name `bar`.
128    #[must_use]
129    pub fn file_name(&self) -> Option<&BStr> {
130        file_name_normalized(self.as_bytes())
131    }
132
133    /// Return the parent portion of this virtual path.
134    ///
135    /// This is a byte-level virtual path operation. It does not interpret `.`,
136    /// `..`, drive prefixes, roots, or host filesystem rules.
137    #[must_use]
138    pub fn parent(&self) -> Option<&BStr> {
139        parent_normalized(self.as_bytes())
140    }
141
142    /// Return the extension of the final component, without the dot.
143    ///
144    /// Dotfiles such as `.hidden`, names ending in `.`, and paths ending in `/`
145    /// have no extension.
146    #[must_use]
147    pub fn extension(&self) -> Option<&BStr> {
148        extension_normalized(self.as_bytes())
149    }
150
151    /// Return true if the normalized path is empty.
152    #[must_use]
153    pub fn is_empty(&self) -> bool {
154        self.0.is_empty()
155    }
156
157    /// Return the normalized path length in bytes.
158    #[must_use]
159    pub fn len(&self) -> usize {
160        self.0.len()
161    }
162}
163
164impl AsRef<[u8]> for NormalizedPath {
165    fn as_ref(&self) -> &[u8] {
166        self.as_bytes()
167    }
168}
169
170impl AsRef<BStr> for NormalizedPath {
171    fn as_ref(&self) -> &BStr {
172        self.as_bstr()
173    }
174}
175
176impl Borrow<[u8]> for NormalizedPath {
177    fn borrow(&self) -> &[u8] {
178        self.as_bytes()
179    }
180}
181
182impl Borrow<BStr> for NormalizedPath {
183    fn borrow(&self) -> &BStr {
184        self.as_bstr()
185    }
186}
187
188impl From<&[u8]> for NormalizedPath {
189    fn from(path: &[u8]) -> Self {
190        Self::new(path)
191    }
192}
193
194impl From<&str> for NormalizedPath {
195    fn from(path: &str) -> Self {
196        Self::new(path)
197    }
198}
199
200impl From<&BStr> for NormalizedPath {
201    fn from(path: &BStr) -> Self {
202        Self::new(path)
203    }
204}
205
206impl From<Vec<u8>> for NormalizedPath {
207    fn from(path: Vec<u8>) -> Self {
208        Self::new(path)
209    }
210}
211
212impl From<String> for NormalizedPath {
213    fn from(path: String) -> Self {
214        Self::new(path)
215    }
216}
217
218impl From<BString> for NormalizedPath {
219    fn from(path: BString) -> Self {
220        Self::new(path)
221    }
222}
223
224impl From<NormalizedPath> for BString {
225    fn from(path: NormalizedPath) -> Self {
226        path.0
227    }
228}
229
230impl From<NormalizedPath> for Vec<u8> {
231    fn from(path: NormalizedPath) -> Self {
232        path.0.into()
233    }
234}
235
236/// Return true if `path` already matches this crate's normalized spelling.
237///
238/// This checks byte spelling only. It does not mean that `path` is a valid
239/// file-like resource, safe host path, URI, display string, or archive path.
240/// Empty paths, trailing separators, NUL bytes, invalid UTF-8, dot segments,
241/// and already-mangled host/URI-looking strings such as `c:/foo` or
242/// `http:/foo/bar` may all be normalized according to this predicate.
243#[must_use]
244pub fn is_normalized_path(path: &[u8]) -> bool {
245    let mut previous_was_separator = true;
246    for &byte in path {
247        match byte {
248            b'\\' | b'A'..=b'Z' => return false,
249            b'/' if previous_was_separator => return false,
250            b'/' => previous_was_separator = true,
251            _ => previous_was_separator = false,
252        }
253    }
254    true
255}
256
257/// Return the final non-empty component of an already-normalized virtual path.
258///
259/// A trailing separator is ignored for component extraction, so `foo/bar/` has
260/// file name `bar`. The input is assumed to satisfy [`is_normalized_path`]; this
261/// function does not normalize, validate resource suitability, or interpret host
262/// filesystem syntax.
263#[must_use]
264pub fn file_name_normalized(path: &[u8]) -> Option<&BStr> {
265    let bytes = without_trailing_separator(path);
266    if bytes.is_empty() {
267        return None;
268    }
269    let start = bytes
270        .iter()
271        .rposition(|byte| *byte == b'/')
272        .map_or(0, |pos| pos + 1);
273    Some(bytes[start..].as_bstr())
274}
275
276/// Return the parent portion of an already-normalized virtual path.
277///
278/// The input is assumed to satisfy [`is_normalized_path`]. This is a byte-level
279/// virtual path operation; it does not resolve `.`, `..`, roots, drive prefixes,
280/// URI schemes, or host filesystem rules.
281#[must_use]
282pub fn parent_normalized(path: &[u8]) -> Option<&BStr> {
283    let bytes = without_trailing_separator(path);
284    let end = bytes.iter().rposition(|byte| *byte == b'/')?;
285    Some(bytes[..end].as_bstr())
286}
287
288/// Return the extension of the final component of an already-normalized virtual
289/// path, without the dot.
290///
291/// Dotfiles such as `.hidden`, names ending in `.`, and paths ending in `/` have
292/// no extension. The input is assumed to satisfy [`is_normalized_path`].
293#[must_use]
294pub fn extension_normalized(path: &[u8]) -> Option<&BStr> {
295    if path.ends_with(b"/") {
296        return None;
297    }
298    let file_name = file_name_normalized(path)?.as_bytes();
299    let dot = file_name.iter().rposition(|byte| *byte == b'.')?;
300    if dot == 0 || dot + 1 == file_name.len() {
301        return None;
302    }
303    Some(file_name[dot + 1..].as_bstr())
304}
305
306/// Normalize a virtual resource path into owned bytes.
307#[must_use]
308pub fn normalize_path(path: impl AsRef<[u8]>) -> Vec<u8> {
309    let path = path.as_ref();
310    let mut out = Vec::with_capacity(path.len());
311    normalize_path_into(&mut out, path);
312    out
313}
314
315/// Normalize an owned virtual resource path, reusing its allocation.
316///
317/// This is a convenience for callers that already own a byte buffer and do not
318/// need to preserve the original spelling. It has the same normalization rules
319/// as [`normalize_path`].
320#[must_use]
321pub fn normalize_path_owned(mut path: Vec<u8>) -> Vec<u8> {
322    normalize_path_in_place(&mut path);
323    path
324}
325
326/// Normalize an owned virtual resource path in place.
327///
328/// The buffer is rewritten using the same rules as [`normalize_path`]. Its
329/// allocation is reused; its length may shrink when leading or repeated
330/// separators are removed.
331pub fn normalize_path_in_place(path: &mut Vec<u8>) {
332    let mut write = 0;
333    let mut previous_was_separator = true;
334    for read in 0..path.len() {
335        let byte = match path[read] {
336            b'\\' => b'/',
337            b'A'..=b'Z' => path[read] + 32,
338            byte => byte,
339        };
340        if byte == b'/' && previous_was_separator {
341            continue;
342        }
343        path[write] = byte;
344        write += 1;
345        previous_was_separator = byte == b'/';
346    }
347    path.truncate(write);
348}
349
350/// Normalize a virtual resource path into an existing buffer.
351///
352/// `out` is cleared before writing. Its previous allocation is reused when
353/// possible. This crate does not enforce a maximum path length; callers handling
354/// untrusted archive, tool, or Lua input should enforce their own byte budget.
355/// A scratch buffer can retain a large allocation after a pathological input, so
356/// discard or shrink it at the caller boundary if that matters.
357pub fn normalize_path_into(out: &mut Vec<u8>, path: &[u8]) {
358    out.clear();
359    out.reserve(path.len());
360    for byte in path.iter().copied() {
361        let byte = match byte {
362            b'\\' => b'/',
363            b'A'..=b'Z' => byte + 32,
364            _ => byte,
365        };
366        if byte == b'/' && (out.is_empty() || out.last() == Some(&b'/')) {
367            continue;
368        }
369        out.push(byte);
370    }
371}
372
373fn without_trailing_separator(bytes: &[u8]) -> &[u8] {
374    bytes.strip_suffix(b"/").unwrap_or(bytes)
375}
376
377#[cfg(test)]
378mod tests {
379    use std::collections::HashMap;
380
381    use bstr::{BStr, BString};
382
383    use super::{
384        NormalizedPath, extension_normalized, file_name_normalized, is_normalized_path,
385        normalize_path, normalize_path_in_place, normalize_path_into, normalize_path_owned,
386        parent_normalized,
387    };
388
389    #[test]
390    fn leaves_empty_path_empty() {
391        assert_eq!(normalize_path(b""), b"");
392    }
393
394    #[test]
395    fn removes_leading_separators() {
396        assert_eq!(normalize_path(b"/foo"), b"foo");
397        assert_eq!(normalize_path(b"///foo//bar"), b"foo/bar");
398    }
399
400    #[test]
401    fn all_separators_normalize_to_empty() {
402        assert_eq!(normalize_path(b"/"), b"");
403        assert_eq!(normalize_path(br"\\\/"), b"");
404    }
405
406    #[test]
407    fn keeps_non_leading_trailing_separator() {
408        assert_eq!(normalize_path(b"foo/"), b"foo/");
409        assert_eq!(normalize_path(br"foo\\"), b"foo/");
410    }
411
412    #[test]
413    fn folds_backslashes_and_ascii_case() {
414        assert_eq!(normalize_path(br"FOO\BaR"), b"foo/bar");
415    }
416
417    #[test]
418    fn collapses_repeated_separators_after_backslash_folding() {
419        assert_eq!(normalize_path(br"foo\\//bar"), b"foo/bar");
420    }
421
422    #[test]
423    fn preserves_non_ascii_bytes() {
424        assert_eq!(normalize_path("Café/Ä".as_bytes()), "café/Ä".as_bytes());
425    }
426
427    #[test]
428    fn only_ascii_uppercase_is_folded() {
429        assert_eq!(normalize_path(b"ABC[\\]^_`XYZ"), b"abc[/]^_`xyz");
430    }
431
432    #[test]
433    fn preserves_invalid_utf8_bytes() {
434        assert_eq!(normalize_path(b"DIR/\xff/FILE"), b"dir/\xff/file");
435    }
436
437    #[test]
438    fn preserves_nul_bytes() {
439        assert_eq!(normalize_path(b"FOO\0BAR"), b"foo\0bar");
440        assert_eq!(normalize_path(b"DIR/\0/FILE"), b"dir/\0/file");
441    }
442
443    #[test]
444    fn does_not_resolve_dot_segments() {
445        assert_eq!(normalize_path(b"A/./B"), b"a/./b");
446        assert_eq!(normalize_path(b"A/../B"), b"a/../b");
447        assert_eq!(
448            NormalizedPath::new(b"Foo/../BAR").parent(),
449            Some(BStr::new(b"foo/.."))
450        );
451    }
452
453    #[test]
454    fn does_not_preserve_uri_or_host_path_syntax() {
455        assert_eq!(normalize_path(b"HTTP://Foo/Bar"), b"http:/foo/bar");
456        assert_eq!(normalize_path(br"C:\Foo"), b"c:/foo");
457        assert_eq!(
458            normalize_path(br"\\Server\Share\File"),
459            b"server/share/file"
460        );
461    }
462
463    #[test]
464    fn trailing_separator_remains_part_of_key() {
465        let file = NormalizedPath::new("textures/foo.dds");
466        let directory_like = NormalizedPath::new("textures/foo.dds/");
467
468        assert_ne!(file, directory_like);
469        assert_eq!(directory_like.as_bytes(), b"textures/foo.dds/");
470    }
471
472    #[test]
473    fn normalization_is_idempotent() {
474        let once = normalize_path(br"//Foo\\BAR///baz");
475        let twice = normalize_path(&once);
476        assert_eq!(once, twice);
477    }
478
479    #[test]
480    fn normalization_invariants_hold_for_byte_corpus() {
481        let mut cases: Vec<Vec<u8>> = (0u8..=u8::MAX).map(|byte| vec![byte]).collect();
482        cases.extend([
483            b"".to_vec(),
484            br"//Foo\\BAR///baz".to_vec(),
485            b"HTTP://Foo/Bar".to_vec(),
486            br"C:\Foo".to_vec(),
487            br"\\Server\Share\File".to_vec(),
488            b"A/./B".to_vec(),
489            b"A/../B".to_vec(),
490            b"DIR/\0/FILE".to_vec(),
491            b"DIR/\xff/FILE".to_vec(),
492            br"/A//B\\C/".to_vec(),
493        ]);
494
495        for case in cases {
496            let normalized = normalize_path(&case);
497            assert!(
498                is_normalized_path(&normalized),
499                "normalized output failed predicate: {case:?}"
500            );
501            assert_eq!(
502                normalize_path(&normalized),
503                normalized,
504                "normalization was not idempotent: {case:?}"
505            );
506            assert!(
507                !normalized.contains(&b'\\'),
508                "backslash survived normalization: {case:?}"
509            );
510            assert!(
511                !normalized.iter().any(u8::is_ascii_uppercase),
512                "uppercase ASCII survived normalization: {case:?}"
513            );
514            assert!(
515                !normalized.starts_with(b"/"),
516                "leading slash survived normalization: {case:?}"
517            );
518            assert!(
519                !normalized.windows(2).any(|window| window == b"//"),
520                "repeated slash survived normalization: {case:?}"
521            );
522        }
523    }
524
525    #[test]
526    fn detects_already_normalized_paths() {
527        assert!(is_normalized_path(b""));
528        assert!(is_normalized_path(b"textures/foo.dds"));
529        assert!(is_normalized_path(b"textures/foo/"));
530        assert!(is_normalized_path(b"textures/\xff/file"));
531        assert!(is_normalized_path(b"foo\0bar"));
532        assert!(is_normalized_path(b"foo/../bar"));
533        assert!(is_normalized_path(b"c:/foo"));
534
535        assert!(!is_normalized_path(b"/textures/foo.dds"));
536        assert!(!is_normalized_path(b"textures//foo.dds"));
537        assert!(!is_normalized_path(br"textures\foo.dds"));
538        assert!(!is_normalized_path(b"textures/FOO.dds"));
539    }
540
541    #[test]
542    fn normalize_into_reuses_and_clears_output() {
543        let mut out = b"stale".to_vec();
544        let capacity = out.capacity();
545        normalize_path_into(&mut out, br"/Foo\Bar");
546        assert_eq!(out, b"foo/bar");
547        assert!(out.capacity() >= capacity);
548    }
549
550    #[test]
551    fn normalize_owned_and_in_place_reuse_existing_storage() {
552        let mut path = br"//Textures\\Foo///BAR.DDS".to_vec();
553        let capacity = path.capacity();
554        normalize_path_in_place(&mut path);
555        assert_eq!(path, b"textures/foo/bar.dds");
556        assert_eq!(path.capacity(), capacity);
557
558        assert_eq!(
559            normalize_path_owned(br"//Meshes\\Door.NIF".to_vec()),
560            b"meshes/door.nif"
561        );
562    }
563
564    #[test]
565    fn normalized_path_exposes_bytes_and_length() {
566        let path = NormalizedPath::new(br"/Meshes\Thing.NIF");
567        assert_eq!(path.as_bytes(), b"meshes/thing.nif");
568        assert_eq!(path.as_bstr(), b"meshes/thing.nif".as_slice());
569        assert_eq!(path.len(), b"meshes/thing.nif".len());
570        assert!(!path.is_empty());
571    }
572
573    #[test]
574    fn normalized_path_reports_utf8_only_when_valid() {
575        assert_eq!(
576            NormalizedPath::new("Textures/Foo.DDS").to_str(),
577            Ok("textures/foo.dds")
578        );
579        assert!(NormalizedPath::new(b"textures/\xff.dds").to_str().is_err());
580        assert_eq!(NormalizedPath::new(b"A\0B").to_str(), Ok("a\0b"));
581    }
582
583    #[test]
584    fn normalized_path_exposes_virtual_components() {
585        let path = NormalizedPath::new(br"/Textures/Architecture/Wall.DDS");
586        assert_eq!(path.parent(), Some(BStr::new(b"textures/architecture")));
587        assert_eq!(path.file_name(), Some(BStr::new(b"wall.dds")));
588        assert_eq!(path.extension(), Some(BStr::new(b"dds")));
589
590        let directory_like = NormalizedPath::new("textures/foo/");
591        assert_eq!(directory_like.parent(), Some(BStr::new(b"textures")));
592        assert_eq!(directory_like.file_name(), Some(BStr::new(b"foo")));
593        assert_eq!(directory_like.extension(), None);
594    }
595
596    #[test]
597    fn normalized_path_extension_is_byte_literal() {
598        assert_eq!(
599            NormalizedPath::new("foo.tar.gz").extension(),
600            Some(BStr::new(b"gz"))
601        );
602        assert_eq!(NormalizedPath::new(".hidden").extension(), None);
603        assert_eq!(NormalizedPath::new("foo.").extension(), None);
604        assert_eq!(NormalizedPath::new("foo.dds/").extension(), None);
605        assert_eq!(
606            NormalizedPath::new(b"foo.\xff").extension(),
607            Some(BStr::new(b"\xff"))
608        );
609    }
610
611    #[test]
612    fn normalized_component_helpers_operate_on_borrowed_bytes() {
613        let path = b"textures/architecture/wall.dds";
614
615        assert_eq!(
616            parent_normalized(path),
617            Some(BStr::new(b"textures/architecture"))
618        );
619        assert_eq!(file_name_normalized(path), Some(BStr::new(b"wall.dds")));
620        assert_eq!(extension_normalized(path), Some(BStr::new(b"dds")));
621        assert_eq!(extension_normalized(b"textures/foo.dds/"), None);
622    }
623
624    #[test]
625    fn checked_normalized_constructor_rejects_unnormalized_bytes() {
626        let path = NormalizedPath::try_from_normalized_bytes(b"textures/foo.dds".to_vec())
627            .expect("path is already normalized");
628        assert_eq!(path.as_bytes(), b"textures/foo.dds");
629        assert_eq!(
630            NormalizedPath::try_from_normalized_bytes(b"textures/foo/".to_vec())
631                .expect("trailing separator is normalized")
632                .as_bytes(),
633            b"textures/foo/"
634        );
635
636        for path in [
637            b"Textures/Foo.DDS".as_slice(),
638            b"/textures/foo.dds".as_slice(),
639            b"textures//foo.dds".as_slice(),
640            br"textures\foo.dds".as_slice(),
641        ] {
642            let rejected = NormalizedPath::try_from_normalized_bytes(path.to_vec())
643                .expect_err("path is not normalized");
644            assert_eq!(rejected, path);
645        }
646    }
647
648    #[test]
649    fn normalized_path_borrows_as_normalized_bytes_for_lookup() {
650        let mut values = HashMap::new();
651        values.insert(NormalizedPath::new(br"/Meshes\Thing.NIF"), 7);
652
653        assert_eq!(values.get(b"meshes/thing.nif".as_slice()), Some(&7));
654        assert_eq!(values.get(BStr::new(b"meshes/thing.nif")), Some(&7));
655    }
656
657    #[test]
658    fn normalized_path_as_ref_supports_byte_and_bstr_views() {
659        let path = NormalizedPath::new(br"/Textures\Foo.DDS");
660        let bytes: &[u8] = path.as_ref();
661        let bstr: &BStr = path.as_ref();
662
663        assert_eq!(bytes, b"textures/foo.dds");
664        assert_eq!(bstr, b"textures/foo.dds".as_slice());
665    }
666
667    #[test]
668    fn normalized_path_converts_into_owned_byte_strings() {
669        let path = NormalizedPath::new(br"/Icons\Foo.TGA");
670        let bstring = BString::from(path.clone());
671        let bytes = Vec::<u8>::from(path);
672
673        assert_eq!(bstring, b"icons/foo.tga".as_slice());
674        assert_eq!(bytes, b"icons/foo.tga");
675    }
676
677    #[test]
678    fn from_impls_normalize() {
679        let bstring = BString::from(b"/Foo".to_vec());
680
681        assert_eq!(NormalizedPath::from("/Foo").as_bytes(), b"foo");
682        assert_eq!(NormalizedPath::from(b"/Foo".as_slice()).as_bytes(), b"foo");
683        assert_eq!(NormalizedPath::from(BStr::new(&bstring)).as_bytes(), b"foo");
684        assert_eq!(
685            NormalizedPath::from(String::from("/Foo")).as_bytes(),
686            b"foo"
687        );
688        assert_eq!(NormalizedPath::from(b"/Foo".to_vec()).as_bytes(), b"foo");
689        assert_eq!(
690            NormalizedPath::from(BString::from(b"/Foo".to_vec())).as_bytes(),
691            b"foo"
692        );
693    }
694}