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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(any(feature = "beth-archives", feature = "zip"))]
use std::sync::Arc;
use std::{
fs::File as StdFile,
io::{self, Read},
path::{Path, PathBuf},
};
#[cfg(any(feature = "beth-archives", feature = "zip"))]
use crate::archives::StoredArchive;
#[cfg(any(feature = "beth-archives", feature = "zip"))]
#[path = "vfs_file/archive.rs"]
mod archive;
#[cfg(any(feature = "beth-archives", feature = "zip"))]
pub use archive::ArchiveReference;
#[cfg(test)]
#[path = "vfs_file/tests.rs"]
mod tests;
/// Backing storage for a [`VfsFile`]: either a loose path on disk or an archive entry.
#[derive(Debug, Clone)]
pub enum FileType {
/// File stored inside a BSA, BA2, ZIP, or PK3 archive.
#[cfg(any(feature = "beth-archives", feature = "zip"))]
Archive(ArchiveReference),
/// Loose file on the real filesystem, stored exactly as the caller or scanner provided it.
Loose(PathBuf),
}
/// Backing file handle stored by the Virtual File System (VFS).
///
/// Loose files keep the host path exactly as supplied by the caller or directory scanner. Archive
/// files keep the original in-archive entry path plus an archive handle. Neither path is normalized
/// here; normalization belongs to VFS keys and provider stacks, not to the object that opens bytes.
///
/// Provider identity is owned by [`crate::VFS`]. The same `VfsFile` value can appear in different
/// provider stacks, and a resolved VFS key is not required to be unique merely because a loose host
/// path is unique. Pretending otherwise is how provenance reports become fiction.
#[derive(Debug, Clone)]
pub struct VfsFile {
file: FileType,
}
impl Default for VfsFile {
fn default() -> Self {
Self {
file: FileType::Loose(PathBuf::default()),
}
}
}
impl VfsFile {
/// Creates a new `VfsFile` instance with the given `path`.
///
/// # Arguments
///
/// * `path` - An owned `PathBuf` representing the file's location on disk.
///
/// # Notes
///
/// - Paths **must not be normalized** at creation time to avoid potential file lookup issues.
/// - `VfsFile` does not, itself, verify that the provided path exists at creation time;
/// this responsibility is left up to its constructor (typically, the VFS struct)
///
/// # Examples
///
/// ```
/// use std::path::PathBuf;
/// use vfstool_lib::VfsFile;
///
/// let path = "C:\\Morrowind\\Data Files\\Morrowind.esm";
///
/// let file = VfsFile::from(path);
/// assert_eq!(file.path().to_str(), Some(path));
/// ```
pub fn from<P: AsRef<Path>>(path: P) -> Self {
VfsFile {
file: FileType::Loose(path.as_ref().to_path_buf()),
}
}
/// Creates a [`VfsFile`] backed by an entry inside `parent_archive`.
///
/// `path` is the in-archive path of the file (not normalized; normalization happens at the
/// VFS key level, not here).
#[cfg(any(feature = "beth-archives", feature = "zip"))]
pub fn from_archive<S: AsRef<str>>(path: S, parent_archive: Arc<StoredArchive>) -> Self {
VfsFile {
file: FileType::Archive(ArchiveReference::new(path.as_ref(), parent_archive)),
}
}
/// Creates a [`VfsFile`] backed by a ZIP entry at a specific central-directory index.
///
/// ZIP archives may contain exact duplicate entry names. The index is therefore part of the
/// provider identity; opening by name would be a causality bug wearing a compression format.
#[cfg(feature = "zip")]
pub fn from_zip_archive<S: AsRef<str>>(
path: S,
zip_index: usize,
parent_archive: Arc<StoredArchive>,
) -> Self {
VfsFile {
file: FileType::Archive(ArchiveReference::new_zip(
path.as_ref(),
zip_index,
parent_archive,
)),
}
}
/// Creates a [`VfsFile`] backed by a byte-named entry inside `parent_archive`.
#[cfg(any(feature = "beth-archives", feature = "zip"))]
pub fn from_archive_bytes(path: &[u8], parent_archive: Arc<StoredArchive>) -> Self {
VfsFile {
file: FileType::Archive(ArchiveReference::from_bytes(path, parent_archive)),
}
}
/// Returns `true` if this file is a loose file on the real filesystem.
#[must_use]
pub fn is_loose(&self) -> bool {
match self.file {
FileType::Loose(_) => true,
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(_) => false,
}
}
/// Returns `true` if this file is stored inside a BSA, BA2, ZIP, or PK3 archive.
#[must_use]
pub fn is_archive(&self) -> bool {
match self.file {
FileType::Loose(_) => false,
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(_) => true,
}
}
/// Returns the stored path to the parent archive as a string, or `None` for loose files.
///
/// This is the path the archive was opened with; it is not canonicalized here.
#[must_use]
pub fn parent_archive_path(&self) -> Option<String> {
match &self.file {
FileType::Loose(_) => None,
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => {
let path_str = archive_ref
.parent_archive
.path()
.to_string_lossy()
.to_string();
Some(path_str)
}
}
}
/// Returns just the file name of the parent archive (e.g. `"Morrowind.bsa"`), or `None` for loose files.
#[must_use]
pub fn parent_archive_name(&self) -> Option<String> {
match &self.file {
FileType::Loose(_) => None,
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => {
let name = archive_ref
.parent_archive
.path()
.file_name()?
.to_string_lossy()
.to_string();
Some(name)
}
}
}
/// Returns an `Arc` clone of the parent archive handle, or an error for loose files.
#[cfg(any(feature = "beth-archives", feature = "zip"))]
///
/// # Errors
///
/// Returns an error when called on a loose-file-backed `VfsFile`.
pub fn parent_archive_handle(&self) -> io::Result<Arc<StoredArchive>> {
match &self.file {
FileType::Loose(_) => Err(io::Error::new(
io::ErrorKind::InvalidData,
"Loose files may not return an archive reference!",
)),
FileType::Archive(archive_ref) => Ok(Arc::clone(&archive_ref.parent_archive)),
}
}
/// Opens the file and returns a reader.
///
/// Loose files are streamed from a standard filesystem handle. Bethesda archive entries use
/// `dream_archive`'s reader API, so callers get ordinary streaming reads there too. ZIP/PK3
/// entries are still buffered by this crate before the returned reader is handed out, with a
/// per-entry cap; that is VFS-level work left for a custom ZIP reader, not something
/// `dream_archive` should know about.
///
/// # Returns
///
/// * `Ok(Box<dyn Read>)` - If the file exists and can be opened/read.
/// * `Err(io::Error)` - If the file does not exist or cannot be opened.
///
/// # Errors
///
/// Returns an error if opening or reading archive/loose file data fails.
///
/// # Examples
///
/// ```
/// use std::path::PathBuf;
/// use vfstool_lib::VfsFile;
///
/// let path = "C:\\Some\\Very\\Long\\Path";
///
/// let file = VfsFile::from(path);
/// let result = file.open();
///
/// assert!(result.is_err());
/// ```
pub fn open(&self) -> io::Result<Box<dyn Read + '_>> {
match &self.file {
FileType::Loose(path) => {
let file = StdFile::open(path)?;
Ok(Box::new(file))
}
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => archive::open(archive_ref),
}
}
/// Retrieves the file name (i.e., the last component of the path).
///
/// # Returns
///
/// * `Some(&str)` - If the path contains a valid file name.
/// * `None` - If the path does not have a file name. This should be a rare exception as any
/// files typically used *will* have extensions, but it is not necessarily mandatory (eg unix
/// binaries)
///
/// # Examples
///
/// ```
/// use std::path::PathBuf;
/// use vfstool_lib::VfsFile;
///
/// let morrowind_esm = PathBuf::from("C:").join("Morrowind").join("Data
/// Files").join("Morrowind.esm");
///
/// let file = VfsFile::from(morrowind_esm);
/// assert_eq!(file.file_name(), Some(std::ffi::OsStr::new("Morrowind.esm")));
/// ```
#[must_use]
pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
match &self.file {
FileType::Loose(path) => path.file_name(),
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => archive_ref.path.file_name(),
}
}
///
/// Retrieves the file name (i.e., the last component of the path), without
/// extensions.
///
/// # Returns
///
/// * `Some(&str)` - If the path contains a valid file name.
/// * `None` - If the path does not have a file name. This should be a rare exception as any
/// files typically used *will* have extensions, but it is not necessarily mandatory (eg unix
/// binaries)
///
/// # Examples
///
/// ```
/// use std::path::PathBuf;
/// use vfstool_lib::VfsFile;
///
/// let morrowind_esm = PathBuf::from("C:").join("Morrowind").join("Data
/// Files").join("Morrowind.esm");
///
/// let file = VfsFile::from(morrowind_esm);
/// assert_eq!(file.file_stem(), Some(std::ffi::OsStr::new("Morrowind")));
/// ```
#[must_use]
pub fn file_stem(&self) -> Option<&std::ffi::OsStr> {
match &self.file {
FileType::Loose(path) => path.file_stem(),
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => archive_ref.path.file_stem(),
}
}
/// Returns the original (non-normalized) path of the file.
///
/// # Returns
///
/// * `&Path` - The path used when creating this `VfsFile`.
///
/// # Examples
///
/// ```
/// use vfstool_lib::VfsFile;
/// use std::path::PathBuf;
///
/// let path = "C:\\Morrowind\\Data Files\\Morrowind.esm";
///
/// let file = VfsFile::from(path);
/// assert_eq!(file.path(), PathBuf::from(path));
/// ```
#[must_use]
pub fn path(&self) -> &Path {
match &self.file {
FileType::Loose(path) => path.as_path(),
#[cfg(any(feature = "beth-archives", feature = "zip"))]
FileType::Archive(archive_ref) => &archive_ref.path,
}
}
}