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