1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// SPDX-License-Identifier: GPL-3.0-only
use super::{MaybeFile, VFS, VFSTuple};
use crate::{
DisplayTree, VfsKeyInput, normalize_host_path,
paths::{key_is_at_or_under_prefix, key_to_string_lossy},
};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
impl VFS {
/// Looks up a file in the VFS after normalizing the key.
///
/// Already-normalized keys skip path conversion and use their byte key
/// directly; host paths and strings are normalized before lookup.
pub fn get_file<P: VfsKeyInput + ?Sized>(&self, path: &P) -> MaybeFile<'_> {
let key = path.to_vfs_key();
self.file_map.get(&key)
}
/// Search the VFS using a case-insensitive regex pattern.
///
/// Returns a filtered [`DisplayTree`] containing only files whose VFS path
/// matches `pattern`. The pattern is compiled with `case_insensitive(true)`.
///
/// # Errors
///
/// Returns `Err` if `pattern` is not a valid regex.
pub fn find_by_regex(
&self,
pattern: &str,
relative: bool,
) -> std::result::Result<DisplayTree, regex::Error> {
let re = regex::RegexBuilder::new(pattern)
.case_insensitive(true)
.build()?;
Ok(self.tree_filtered(relative, |key, _file| {
re.is_match(&key_to_string_lossy(key))
}))
}
/// Return a filtered tree showing files from or replacing `filter_path`.
///
/// `all_dirs` is the full ordered list of data directories (as from openmw.cfg).
/// The method builds a single-directory VFS for `filter_path`, then filters
/// the full VFS accordingly.
///
/// When `replacements_only` is `false`: files still served from `filter_path`.
/// When `replacements_only` is `true`: files where `filter_path` has a copy
/// but the full VFS serves them from a different (higher-priority) source.
#[must_use]
pub fn remaining(
&self,
filter_path: &Path,
replacements_only: bool,
all_dirs: &[PathBuf],
relative: bool,
) -> DisplayTree {
let filter_normalized = normalize_host_path(filter_path).into_owned();
let filtered_dirs: Vec<&PathBuf> = all_dirs
.iter()
.filter(|d| normalize_host_path(d.as_path()) == filter_normalized.as_path())
.collect();
let filtered_vfs = VFS::from_directories(filtered_dirs, None);
self.tree_filtered(relative, |key, file| {
let file_path = normalize_host_path(file.path()).into_owned();
if replacements_only {
filtered_vfs.contains(key) && !path_is_at_or_under(&file_path, &filter_normalized)
} else {
path_is_at_or_under(&file_path, &filter_normalized)
}
})
}
/// Given a substring, return an iterator over all paths that contain it.
pub fn paths_matching<S: AsRef<str>>(
&self,
substring: S,
) -> impl Iterator<Item = VFSTuple<'_>> {
let needle = Self::normalize_substring(substring);
self.file_map.iter().filter_map(move |(path, file)| {
key_to_string_lossy(path)
.contains(&needle)
.then_some((path, file))
})
}
/// Given a substring, return a parallel iterator over all paths that contain it.
pub fn par_paths_matching<S: AsRef<str>>(
&self,
substring: S,
) -> impl ParallelIterator<Item = VFSTuple<'_>> {
let needle = Self::normalize_substring(substring);
self.file_map.par_iter().filter_map(move |(path, file)| {
key_to_string_lossy(path)
.contains(&needle)
.then_some((path, file))
})
}
/// Given a path prefix to a location in the VFS, return an iterator to *all* of its contents.
pub fn paths_with<P: VfsKeyInput + ?Sized>(
&self,
prefix: &P,
) -> impl Iterator<Item = VFSTuple<'_>> {
let normalized_prefix = prefix.to_vfs_key();
self.file_map.iter().filter_map(move |(path, file)| {
key_is_at_or_under_prefix(path, &normalized_prefix).then_some((path, file))
})
}
/// Given a path prefix to a location in the VFS, return a parallel iterator to *all* of its contents.
pub fn par_paths_with<P: VfsKeyInput + ?Sized>(
&self,
prefix: &P,
) -> impl ParallelIterator<Item = VFSTuple<'_>> {
let normalized_prefix = prefix.to_vfs_key();
self.file_map.par_iter().filter_map(move |(path, file)| {
key_is_at_or_under_prefix(path, &normalized_prefix).then_some((path, file))
})
}
fn normalize_substring<S: AsRef<str>>(s: S) -> String {
normalize_host_path(s.as_ref())
.to_string_lossy()
.into_owned()
}
/// Returns `true` if the VFS contains a file at `key`.
///
/// `key` is a normalized relative VFS path (e.g. `"textures/foo.dds"`).
/// The path is normalized before lookup, so case and separator variants
/// are accepted. Already-normalized keys skip the allocation.
#[must_use]
pub fn contains<K: VfsKeyInput + ?Sized>(&self, key: &K) -> bool {
let key = key.to_vfs_key();
self.file_map.contains_key(&key)
}
}
fn path_is_at_or_under(path: &Path, root: &Path) -> bool {
path == root || path.strip_prefix(root).is_ok()
}