vfstool_lib 0.9.0

A library for constructing and manipulating virtual file systems in Rust, based on OpenMW's VFS implementation.
Documentation
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// SPDX-License-Identifier: GPL-3.0-only
use crate::NormalizedKey;
use ahash::AHashMap;
use std::{collections::BTreeMap, path::PathBuf};

/// Current serialized lock manifest schema version.
pub const VFS_LOCK_SCHEMA_VERSION: u32 = 1;

mod candidate;
mod drift;
mod impact;
mod layer;
mod lock;
mod provenance;
mod provider_io;
mod semantic_conflicts;
mod simulate;

/// Source type in the load order.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum SourceKind {
    /// A loose data directory.
    LooseDir,
    /// An archive source (BSA/BA2/ZIP/PK3).
    Archive,
}

/// A source entry in load-order position.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct SourceMeta {
    /// Absolute path to the source.
    pub path: PathBuf,
    /// Source type.
    pub kind: SourceKind,
}

/// Canonical provider-occurrence index for all normalized VFS keys.
///
/// `path_to_sources[key]` is ordered low -> high priority and preserves provider occurrences,
/// including multiple entries from the same source that normalize to one VFS key.
#[derive(Debug, Clone)]
pub struct LayerIndex {
    /// Sources in load-order position.
    pub sources: Vec<SourceMeta>,
    path_to_sources: AHashMap<NormalizedKey, Vec<usize>>,
    provider_paths: AHashMap<(usize, NormalizedKey), Vec<PathBuf>>,
}

/// One provider entry in a [`LayerIndex`] chain.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct LayerProvider {
    /// Source index in low-to-high priority order.
    pub source_index: usize,
    /// Provider occurrence index within this key's low-to-high provider chain.
    pub provider_index: usize,
    /// Source metadata.
    pub source: SourceMeta,
    /// Normalized VFS key.
    pub key: std::path::PathBuf,
    /// Original path recorded for this provider.
    pub original_path: std::path::PathBuf,
}

/// Provider-occurrence contribution counts for one source.
///
/// Counts are per provider occurrence, not per distinct normalized key. A single source with two
/// original paths that normalize to the same VFS key contributes two occurrences. Same-source
/// duplicate occurrences are still not counted as cross-source overrides.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct SourceContribution {
    /// Source index in low-to-high priority order.
    pub source_index: usize,
    /// Source metadata.
    pub source: SourceMeta,
    /// Number of provider occurrences where this source is the highest-priority provider.
    pub winning_files: usize,
    /// Number of provider occurrences where this source overrides at least one lower-priority provider.
    /// Same-source duplicate occurrences are not counted as cross-source overrides.
    pub overriding_files: usize,
    /// Number of provider occurrences where this source is overridden by a higher-priority provider.
    /// Same-source duplicate occurrences are not counted as cross-source overrides.
    pub overridden_files: usize,
    /// Number of provider occurrences whose normalized key has no other providers.
    pub unique_files: usize,
    /// Number of provider occurrences that share a normalized key with another provider.
    pub duplicate_files: usize,
    /// Number of loose-file providers from this source.
    pub loose_files: usize,
    /// Number of archive-entry providers from this source.
    pub archive_files: usize,
}

/// Contribution report for every source.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct SourceContributionReport {
    /// Per-source contribution rows.
    pub sources: Vec<SourceContribution>,
}

/// One provider in a per-key provenance chain.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct ProviderRecord {
    /// Source metadata.
    pub source: SourceMeta,
    /// Absolute loose path or archive-entry display path.
    pub resolved_path: String,
    /// Optional content hash (unavailable for some archive providers).
    pub hash_blake3: Option<String>,
    /// Optional byte size.
    pub size: Option<u64>,
}

/// Full load-order chain for a key.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct ProvenanceChain {
    /// Normalized key queried.
    pub key: PathBuf,
    /// Providers in low -> high priority order.
    pub providers: Vec<ProviderRecord>,
    /// Winning source.
    pub winner: SourceMeta,
}

/// Deterministic lock file output.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct VfsLock {
    /// Schema version.
    pub schema_version: u32,
    /// Deterministically sorted lock entries.
    pub entries: Vec<VfsLockEntry>,
}

/// One deterministic lock entry.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct VfsLockEntry {
    /// Normalized key.
    pub key: PathBuf,
    /// Winning source.
    pub winner_source: PathBuf,
    /// Winner source kind.
    pub winner_kind: SourceKind,
    /// Winner hash (hex) when available.
    pub winner_hash_blake3: Option<String>,
    /// Winner size when available.
    pub winner_size: Option<u64>,
    /// Number of providers.
    pub provider_count: usize,
}

/// Reorder operation for what-if simulations.
#[derive(Debug, Clone)]
pub enum ReorderOp {
    /// Swap two sources by exact path.
    Swap(PathBuf, PathBuf),
    /// Move one source before another source.
    MoveBefore {
        /// Source to move.
        source: PathBuf,
        /// Destination source before which `source` is inserted.
        before: PathBuf,
    },
    /// Move one source after another source.
    MoveAfter {
        /// Source to move.
        source: PathBuf,
        /// Destination source after which `source` is inserted.
        after: PathBuf,
    },
    /// Set the full explicit load order.
    FullOrder(Vec<PathBuf>),
}

/// Simulation options.
#[derive(Debug, Clone)]
pub struct SimOpts {
    /// Maximum number of changed keys included in sample output.
    pub sample_limit: usize,
    /// Optional impact bucket globs (e.g. `textures/**`, `meshes/**`).
    pub impact_buckets: Vec<String>,
}

/// Condition under which an impact rule applies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
pub enum HeuristicCondition {
    /// Applies whenever the winner changed for the key.
    WinnerChanged,
    /// Applies only when winner changed and semantic analysis marks behavior change.
    WinnerChangedAndSemanticBehaviorChanging,
}

/// One weighted impact heuristic.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct ImpactHeuristic {
    /// Rule name used in diagnostics.
    pub name: String,
    /// Path glob for rule scope.
    pub path_glob: String,
    /// Rule weight added to total score when matched.
    pub weight: f32,
    /// Match condition for this rule.
    pub condition: HeuristicCondition,
}

/// Impact scoring profile.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct ImpactProfile {
    /// Ordered list of weighted heuristics.
    pub heuristics: Vec<ImpactHeuristic>,
}

/// One risky changed key row.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct RiskyChange {
    /// Changed key.
    pub key: PathBuf,
    /// Accumulated impact score.
    pub score: f32,
    /// Names of matched heuristic rules.
    pub reasons: Vec<String>,
}

/// Impact score aggregate per bucket glob.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BucketImpact {
    /// Bucket glob.
    pub bucket: String,
    /// Summed score for changed keys in this bucket.
    pub score: f32,
}

/// Impact scoring result.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct ImpactReport {
    /// Total accumulated score across changed keys.
    pub overall_score: f32,
    /// Coarse risk level derived from total score.
    pub risk_level: RiskLevel,
    /// Per-bucket impact summaries.
    pub by_bucket: Vec<BucketImpact>,
    /// Top changed keys ranked by impact score.
    pub top_risky_changes: Vec<RiskyChange>,
}

impl Default for SimOpts {
    fn default() -> Self {
        Self {
            sample_limit: 100,
            impact_buckets: Vec::new(),
        }
    }
}

/// Change count for one impact bucket.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct BucketDelta {
    /// Bucket glob.
    pub bucket: String,
    /// Count of changed winners that matched this bucket.
    pub changed_winners: usize,
}

/// Per-source win delta in a simulation.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct SourceDelta {
    /// Source path.
    pub source: PathBuf,
    /// Wins before simulation.
    pub wins_before: usize,
    /// Wins after simulation.
    pub wins_after: usize,
}

/// Summary of a what-if simulation.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct SimulationDelta {
    /// Number of keys with different winners.
    pub changed_winners: usize,
    /// Number of keys with unchanged winners.
    pub unchanged_winners: usize,
    /// Per-source win deltas.
    pub by_source_gain_loss: Vec<SourceDelta>,
    /// Change totals by optional bucket globs.
    pub by_bucket: Vec<BucketDelta>,
    /// Small sorted sample of changed keys.
    pub changed_keys_sample: Vec<PathBuf>,
}

/// Per-key drift kind when comparing current state to a lock file.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
pub enum DriftKind {
    /// Key exists now but not in lock.
    Added,
    /// Key exists in lock but not now.
    Removed,
    /// Winner source path changed.
    WinnerSourceChanged,
    /// Winner content hash changed.
    WinnerHashChanged,
    /// Provider count changed.
    ProviderCountChanged,
}

/// One drift report row.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct DriftEntry {
    /// Key whose lock relation drifted.
    pub key: PathBuf,
    /// Drift category.
    pub kind: DriftKind,
}

/// Drift report against a lock manifest.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct DriftReport {
    /// Per-key drift entries.
    pub entries: Vec<DriftEntry>,
    /// Aggregated counts by drift kind.
    pub counts: BTreeMap<DriftKind, usize>,
}

/// Optional risk level for candidate planning workflows.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
pub enum RiskLevel {
    /// Lowest risk.
    Low,
    /// Medium risk.
    Medium,
    /// High risk.
    High,
    /// Highest risk.
    Critical,
}

/// Candidate planning options.
#[derive(Debug, Clone, Copy)]
pub struct CandidatePlanOpts {
    /// Include semantic equality checks for conflicting files.
    pub include_semantic: bool,
}

impl Default for CandidatePlanOpts {
    fn default() -> Self {
        Self {
            include_semantic: true,
        }
    }
}

/// One conflict row in a candidate preflight plan.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct CandidateConflict {
    /// Normalized key.
    pub key: PathBuf,
    /// Current winner source path.
    pub current_winner_source: PathBuf,
    /// Candidate file path.
    pub candidate_file: PathBuf,
    /// Whether candidate content differs from current winner.
    pub semantic_differs: Option<bool>,
    /// Optional risk level placeholder.
    pub risk: Option<RiskLevel>,
}

/// Candidate planner summary metrics.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct CandidatePlanSummary {
    /// Count of net-new files.
    pub additions: usize,
    /// Count of path conflicts.
    pub conflicts: usize,
    /// Count of keys whose winner would be displaced.
    pub displaced_winners: usize,
}

/// Candidate preflight plan.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct CandidatePlan {
    /// Normalized keys that would be newly added.
    pub additions: Vec<PathBuf>,
    /// Conflicting keys and metadata.
    pub conflicts: Vec<CandidateConflict>,
    /// Keys whose current winners would be replaced by candidate content.
    pub displaced_winners: Vec<PathBuf>,
    /// Summary counters.
    pub summary: CandidatePlanSummary,
}
#[cfg(test)]
#[path = "analysis/tests.rs"]
mod tests;