create-feltdb 0.11.7 → 0.11.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/lib.rs +363 -2
- package/dist/server-source/crates/feltdb/src/read_basis.rs +257 -0
- package/dist/server-source/crates/feltdb/src/state_contract.rs +442 -73
- package/dist/server-source/crates/feltdb/src/workload_diagnostics.rs +18 -4
- package/dist/server-source/crates/feltdb-server/src/backup.rs +290 -7
- package/dist/server-source/crates/feltdb-server/src/main.rs +500 -23
- package/package.json +1 -1
- package/dist/server-source/crates/feltdb-wasm/test.db +0 -0
|
@@ -135,10 +135,18 @@ pub enum Phase {
|
|
|
135
135
|
/// AuditWrite` is therefore routing plus decode plus encode, measured as a
|
|
136
136
|
/// difference of two spans rather than asserted.
|
|
137
137
|
HandlerBody,
|
|
138
|
+
/// Resolving a managed request's application contract: the revision, the
|
|
139
|
+
/// runtime inventory it is validated against, and the resolved runtime.
|
|
140
|
+
/// Every `/v1` request pays it, so it must not depend on unrelated state.
|
|
141
|
+
ContractResolution,
|
|
142
|
+
/// Cloning one state namespace's rows for a state-contract read or
|
|
143
|
+
/// transaction. Proportional to that application's stored rows, not to
|
|
144
|
+
/// other applications'.
|
|
145
|
+
NamespaceSnapshot,
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
impl Phase {
|
|
141
|
-
pub const ALL: [Phase;
|
|
149
|
+
pub const ALL: [Phase; 27] = [
|
|
142
150
|
Phase::StateLockWait,
|
|
143
151
|
Phase::StateLockHold,
|
|
144
152
|
Phase::IndexedQuery,
|
|
@@ -164,6 +172,8 @@ impl Phase {
|
|
|
164
172
|
Phase::AuditGroupSync,
|
|
165
173
|
Phase::AuditClose,
|
|
166
174
|
Phase::HandlerBody,
|
|
175
|
+
Phase::ContractResolution,
|
|
176
|
+
Phase::NamespaceSnapshot,
|
|
167
177
|
];
|
|
168
178
|
|
|
169
179
|
/// The stable name this phase is reported under.
|
|
@@ -194,6 +204,8 @@ impl Phase {
|
|
|
194
204
|
Phase::AuditGroupSync => "audit_group_sync",
|
|
195
205
|
Phase::AuditClose => "audit_close",
|
|
196
206
|
Phase::HandlerBody => "handler_body",
|
|
207
|
+
Phase::ContractResolution => "contract_resolution",
|
|
208
|
+
Phase::NamespaceSnapshot => "namespace_snapshot",
|
|
197
209
|
}
|
|
198
210
|
}
|
|
199
211
|
|
|
@@ -210,9 +222,11 @@ impl Phase {
|
|
|
210
222
|
// The predicate runs on both executions, so its parent is the lock
|
|
211
223
|
// rather than either query phase.
|
|
212
224
|
Phase::PredicateEvaluation => Some(Phase::StateLockHold),
|
|
213
|
-
Phase::Authorization
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
Phase::Authorization
|
|
226
|
+
| Phase::AuditWrite
|
|
227
|
+
| Phase::HandlerBody
|
|
228
|
+
| Phase::ContractResolution
|
|
229
|
+
| Phase::NamespaceSnapshot => Some(Phase::HttpHandler),
|
|
216
230
|
Phase::Ordering | Phase::ResultMaterialization => Some(Phase::HandlerBody),
|
|
217
231
|
Phase::RecordMutation | Phase::IndexMaintenance | Phase::Persistence => {
|
|
218
232
|
Some(Phase::StateLockHold)
|
|
@@ -4,11 +4,32 @@ use std::{
|
|
|
4
4
|
fs::{self, File},
|
|
5
5
|
io::{self, Read, Write},
|
|
6
6
|
path::{Path, PathBuf},
|
|
7
|
-
time::{SystemTime, UNIX_EPOCH},
|
|
7
|
+
time::{Instant, SystemTime, UNIX_EPOCH},
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
pub const BACKUP_FORMAT: u32 = 1;
|
|
11
11
|
|
|
12
|
+
/// Where the backed-up state came from. Optional in the manifest so that
|
|
13
|
+
/// bundles written before provenance was recorded still verify: the format is
|
|
14
|
+
/// unchanged, and an absent field is reported as unknown rather than invented.
|
|
15
|
+
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
16
|
+
pub struct BackupSource {
|
|
17
|
+
/// Absolute path of the durable state file the bundle was taken from.
|
|
18
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
19
|
+
pub data: Option<String>,
|
|
20
|
+
/// SHA-256 of the authoritative state artifact (`state.log`, or the
|
|
21
|
+
/// consistent `database.snapshot.json` of an online backup).
|
|
22
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
23
|
+
pub state_sha256: Option<String>,
|
|
24
|
+
/// The `feltdb-server` crate version that wrote the bundle (not the npm
|
|
25
|
+
/// package version).
|
|
26
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
27
|
+
pub server_version: Option<String>,
|
|
28
|
+
/// The durable format version of the build that wrote the bundle.
|
|
29
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
30
|
+
pub durable_format: Option<u32>,
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
13
34
|
pub struct BackupEntry {
|
|
14
35
|
pub path: String,
|
|
@@ -21,6 +42,93 @@ pub struct BackupManifest {
|
|
|
21
42
|
pub format: u32,
|
|
22
43
|
pub created_ms: u64,
|
|
23
44
|
pub entries: Vec<BackupEntry>,
|
|
45
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
46
|
+
pub source: Option<BackupSource>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The explicit outcome of a backup operation.
|
|
50
|
+
///
|
|
51
|
+
/// `status` is `ok` only when the bundle was written *and* independently
|
|
52
|
+
/// verified; the CLI exit code follows it, so a successful backup is never
|
|
53
|
+
/// reported as a failure and an unverifiable one is never reported as valid.
|
|
54
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
55
|
+
pub struct BackupReport {
|
|
56
|
+
pub operation: String,
|
|
57
|
+
pub status: String,
|
|
58
|
+
/// Absolute path of the bundle.
|
|
59
|
+
pub path: String,
|
|
60
|
+
/// SHA-256 of the bundle manifest: the bundle's stable identity.
|
|
61
|
+
pub backup_id: String,
|
|
62
|
+
pub source: BackupSource,
|
|
63
|
+
pub format: u32,
|
|
64
|
+
pub created_ms: u64,
|
|
65
|
+
pub files: usize,
|
|
66
|
+
pub bytes_written: u64,
|
|
67
|
+
/// `verified` or `failed`.
|
|
68
|
+
pub verification: String,
|
|
69
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
70
|
+
pub error: Option<String>,
|
|
71
|
+
pub duration_ms: u64,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Resolve a user-supplied path against the working directory.
|
|
75
|
+
///
|
|
76
|
+
/// A bare relative path such as `backup` has an *empty* parent, not `.`; every
|
|
77
|
+
/// operation that syncs or stages next to the output therefore works on the
|
|
78
|
+
/// absolute form, which is what makes `backup`, `./backup` and
|
|
79
|
+
/// `/abs/path/backup` behave identically.
|
|
80
|
+
pub fn normalize_path(path: &Path) -> io::Result<PathBuf> {
|
|
81
|
+
if path.as_os_str().is_empty() {
|
|
82
|
+
return Err(io::Error::new(
|
|
83
|
+
io::ErrorKind::InvalidInput,
|
|
84
|
+
"backup path must not be empty",
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
std::path::absolute(path)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// SHA-256 of a bundle's manifest bytes, used as the bundle identity.
|
|
91
|
+
pub fn backup_id(archive: &Path) -> io::Result<String> {
|
|
92
|
+
Ok(format!(
|
|
93
|
+
"{:x}",
|
|
94
|
+
Sha256::digest(fs::read(archive.join("manifest.json"))?)
|
|
95
|
+
))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Verify a bundle and describe it as a report, without ever turning a
|
|
99
|
+
/// verification failure into success.
|
|
100
|
+
pub fn verification_report(archive: &Path, started: Instant, operation: &str) -> BackupReport {
|
|
101
|
+
let path = normalize_path(archive).unwrap_or_else(|_| archive.to_path_buf());
|
|
102
|
+
match verify_bundle(&path).and_then(|manifest| Ok((backup_id(&path)?, manifest))) {
|
|
103
|
+
Ok((id, manifest)) => BackupReport {
|
|
104
|
+
operation: operation.into(),
|
|
105
|
+
status: "ok".into(),
|
|
106
|
+
path: path.display().to_string(),
|
|
107
|
+
backup_id: id,
|
|
108
|
+
source: manifest.source.clone().unwrap_or_default(),
|
|
109
|
+
format: manifest.format,
|
|
110
|
+
created_ms: manifest.created_ms,
|
|
111
|
+
files: manifest.entries.len(),
|
|
112
|
+
bytes_written: manifest.entries.iter().map(|entry| entry.bytes).sum(),
|
|
113
|
+
verification: "verified".into(),
|
|
114
|
+
error: None,
|
|
115
|
+
duration_ms: started.elapsed().as_millis() as u64,
|
|
116
|
+
},
|
|
117
|
+
Err(error) => BackupReport {
|
|
118
|
+
operation: operation.into(),
|
|
119
|
+
status: "failed".into(),
|
|
120
|
+
path: path.display().to_string(),
|
|
121
|
+
backup_id: backup_id(&path).unwrap_or_default(),
|
|
122
|
+
source: BackupSource::default(),
|
|
123
|
+
format: 0,
|
|
124
|
+
created_ms: 0,
|
|
125
|
+
files: 0,
|
|
126
|
+
bytes_written: 0,
|
|
127
|
+
verification: "failed".into(),
|
|
128
|
+
error: Some(error.to_string()),
|
|
129
|
+
duration_ms: started.elapsed().as_millis() as u64,
|
|
130
|
+
},
|
|
131
|
+
}
|
|
24
132
|
}
|
|
25
133
|
|
|
26
134
|
pub fn server_artifacts(data: &Path, keys: &Path, audit: &Path) -> Vec<(String, PathBuf)> {
|
|
@@ -72,6 +180,20 @@ pub fn create_bundle_with_virtual(
|
|
|
72
180
|
artifacts: &[(String, PathBuf)],
|
|
73
181
|
virtual_files: &[(String, Vec<u8>)],
|
|
74
182
|
) -> io::Result<BackupManifest> {
|
|
183
|
+
create_bundle_with_source(output, artifacts, virtual_files, BackupSource::default())
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Write a bundle and record where its state came from.
|
|
187
|
+
///
|
|
188
|
+
/// The bundle is staged beside the output and published with one rename, so a
|
|
189
|
+
/// reader sees either no bundle or a complete one.
|
|
190
|
+
pub fn create_bundle_with_source(
|
|
191
|
+
output: &Path,
|
|
192
|
+
artifacts: &[(String, PathBuf)],
|
|
193
|
+
virtual_files: &[(String, Vec<u8>)],
|
|
194
|
+
mut source: BackupSource,
|
|
195
|
+
) -> io::Result<BackupManifest> {
|
|
196
|
+
let output = &normalize_path(output)?;
|
|
75
197
|
if output.exists() {
|
|
76
198
|
return Err(io::Error::new(
|
|
77
199
|
io::ErrorKind::AlreadyExists,
|
|
@@ -114,6 +236,14 @@ pub fn create_bundle_with_virtual(
|
|
|
114
236
|
});
|
|
115
237
|
}
|
|
116
238
|
entries.sort_by(|left, right| left.path.cmp(&right.path));
|
|
239
|
+
if source.state_sha256.is_none() {
|
|
240
|
+
source.state_sha256 = entries
|
|
241
|
+
.iter()
|
|
242
|
+
.find(|entry| {
|
|
243
|
+
entry.path == "files/database.snapshot.json" || entry.path == "files/state.log"
|
|
244
|
+
})
|
|
245
|
+
.map(|entry| entry.sha256.clone());
|
|
246
|
+
}
|
|
117
247
|
let manifest = BackupManifest {
|
|
118
248
|
format: BACKUP_FORMAT,
|
|
119
249
|
created_ms: SystemTime::now()
|
|
@@ -121,6 +251,7 @@ pub fn create_bundle_with_virtual(
|
|
|
121
251
|
.unwrap_or_default()
|
|
122
252
|
.as_millis() as u64,
|
|
123
253
|
entries,
|
|
254
|
+
source: Some(source),
|
|
124
255
|
};
|
|
125
256
|
write_synced(
|
|
126
257
|
&staging.join("manifest.json"),
|
|
@@ -128,9 +259,7 @@ pub fn create_bundle_with_virtual(
|
|
|
128
259
|
)?;
|
|
129
260
|
sync_directory(&staging)?;
|
|
130
261
|
fs::rename(&staging, output)?;
|
|
131
|
-
|
|
132
|
-
sync_directory(parent)?;
|
|
133
|
-
}
|
|
262
|
+
sync_parent(output)?;
|
|
134
263
|
Ok(manifest)
|
|
135
264
|
})();
|
|
136
265
|
if result.is_err() {
|
|
@@ -140,6 +269,7 @@ pub fn create_bundle_with_virtual(
|
|
|
140
269
|
}
|
|
141
270
|
|
|
142
271
|
pub fn verify_bundle(archive: &Path) -> io::Result<BackupManifest> {
|
|
272
|
+
let archive = &normalize_path(archive)?;
|
|
143
273
|
if archive.with_extension("partial").exists() {
|
|
144
274
|
return Err(io::Error::new(
|
|
145
275
|
io::ErrorKind::InvalidData,
|
|
@@ -167,6 +297,25 @@ pub fn verify_bundle(archive: &Path) -> io::Result<BackupManifest> {
|
|
|
167
297
|
));
|
|
168
298
|
}
|
|
169
299
|
}
|
|
300
|
+
// Completeness runs both ways: every manifest entry is present and intact
|
|
301
|
+
// (above), and nothing unrecorded sits in the bundle. An extra file would be
|
|
302
|
+
// restored as state that no checksum ever covered.
|
|
303
|
+
let recorded: std::collections::BTreeSet<&str> = manifest
|
|
304
|
+
.entries
|
|
305
|
+
.iter()
|
|
306
|
+
.map(|entry| entry.path.as_str())
|
|
307
|
+
.collect();
|
|
308
|
+
let mut present = Vec::new();
|
|
309
|
+
collect_files(&archive.join("files"), "files", &mut present)?;
|
|
310
|
+
if let Some(extra) = present
|
|
311
|
+
.iter()
|
|
312
|
+
.find(|path| !recorded.contains(path.as_str()))
|
|
313
|
+
{
|
|
314
|
+
return Err(io::Error::new(
|
|
315
|
+
io::ErrorKind::InvalidData,
|
|
316
|
+
format!("backup contains a file absent from its manifest: {extra}"),
|
|
317
|
+
));
|
|
318
|
+
}
|
|
170
319
|
Ok(manifest)
|
|
171
320
|
}
|
|
172
321
|
|
|
@@ -174,6 +323,7 @@ pub fn verify_bundle(archive: &Path) -> io::Result<BackupManifest> {
|
|
|
174
323
|
/// does not exceed `at_ms`. Every manifest-bearing child is verified first so a
|
|
175
324
|
/// corrupt recent backup cannot cause a silent fallback to older data.
|
|
176
325
|
pub fn select_bundle_at(catalog: &Path, at_ms: u64) -> io::Result<PathBuf> {
|
|
326
|
+
let catalog = &normalize_path(catalog)?;
|
|
177
327
|
let mut candidates = Vec::new();
|
|
178
328
|
for entry in fs::read_dir(catalog)? {
|
|
179
329
|
let path = entry?.path();
|
|
@@ -198,6 +348,8 @@ pub fn select_bundle_at(catalog: &Path, at_ms: u64) -> io::Result<PathBuf> {
|
|
|
198
348
|
}
|
|
199
349
|
|
|
200
350
|
pub fn restore_bundle(archive: &Path, output: &Path) -> io::Result<BackupManifest> {
|
|
351
|
+
let archive = &normalize_path(archive)?;
|
|
352
|
+
let output = &normalize_path(output)?;
|
|
201
353
|
let manifest = verify_bundle(archive)?;
|
|
202
354
|
if output.exists() {
|
|
203
355
|
return Err(io::Error::new(
|
|
@@ -221,9 +373,7 @@ pub fn restore_bundle(archive: &Path, output: &Path) -> io::Result<BackupManifes
|
|
|
221
373
|
)?;
|
|
222
374
|
sync_directory(&staging)?;
|
|
223
375
|
fs::rename(&staging, output)?;
|
|
224
|
-
|
|
225
|
-
sync_directory(parent)?;
|
|
226
|
-
}
|
|
376
|
+
sync_parent(output)?;
|
|
227
377
|
Ok(manifest.clone())
|
|
228
378
|
})();
|
|
229
379
|
if result.is_err() {
|
|
@@ -271,6 +421,22 @@ fn copy_source(
|
|
|
271
421
|
}
|
|
272
422
|
Ok(())
|
|
273
423
|
}
|
|
424
|
+
fn collect_files(directory: &Path, relative: &str, found: &mut Vec<String>) -> io::Result<()> {
|
|
425
|
+
if !directory.exists() {
|
|
426
|
+
return Ok(());
|
|
427
|
+
}
|
|
428
|
+
for child in fs::read_dir(directory)? {
|
|
429
|
+
let child = child?;
|
|
430
|
+
let name = format!("{relative}/{}", child.file_name().to_string_lossy());
|
|
431
|
+
let kind = child.file_type()?;
|
|
432
|
+
if kind.is_dir() {
|
|
433
|
+
collect_files(&child.path(), &name, found)?;
|
|
434
|
+
} else {
|
|
435
|
+
found.push(name);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
Ok(())
|
|
439
|
+
}
|
|
274
440
|
fn copy_tree(source: &Path, destination: &Path) -> io::Result<()> {
|
|
275
441
|
if !source.exists() {
|
|
276
442
|
return Ok(());
|
|
@@ -322,6 +488,14 @@ fn sync_file(path: &Path) -> io::Result<()> {
|
|
|
322
488
|
fn sync_directory(path: &Path) -> io::Result<()> {
|
|
323
489
|
File::open(path)?.sync_all()
|
|
324
490
|
}
|
|
491
|
+
/// Sync the directory that holds `path`. A relative path with no directory
|
|
492
|
+
/// component lives in the working directory; its parent is `.`, never `""`.
|
|
493
|
+
fn sync_parent(path: &Path) -> io::Result<()> {
|
|
494
|
+
match path.parent() {
|
|
495
|
+
Some(parent) if !parent.as_os_str().is_empty() => sync_directory(parent),
|
|
496
|
+
_ => sync_directory(Path::new(".")),
|
|
497
|
+
}
|
|
498
|
+
}
|
|
325
499
|
|
|
326
500
|
#[cfg(test)]
|
|
327
501
|
mod tests {
|
|
@@ -409,4 +583,113 @@ mod tests {
|
|
|
409
583
|
assert!(select_bundle_at(&root.join("catalog"), first.created_ms - 1).is_err());
|
|
410
584
|
fs::remove_dir_all(root).unwrap();
|
|
411
585
|
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn relative_and_absolute_paths_normalize_identically() {
|
|
589
|
+
let cwd = std::env::current_dir().unwrap();
|
|
590
|
+
assert_eq!(
|
|
591
|
+
normalize_path(Path::new("backup")).unwrap(),
|
|
592
|
+
cwd.join("backup")
|
|
593
|
+
);
|
|
594
|
+
assert_eq!(
|
|
595
|
+
normalize_path(Path::new("./backup")).unwrap(),
|
|
596
|
+
cwd.join("./backup")
|
|
597
|
+
);
|
|
598
|
+
assert_eq!(
|
|
599
|
+
normalize_path(&cwd.join("backup")).unwrap(),
|
|
600
|
+
cwd.join("backup")
|
|
601
|
+
);
|
|
602
|
+
assert!(normalize_path(Path::new("")).is_err());
|
|
603
|
+
// The defect: a bare name has an empty parent, which cannot be opened
|
|
604
|
+
// for the post-rename directory sync. `sync_parent` must treat it as `.`.
|
|
605
|
+
assert_eq!(Path::new("backup").parent(), Some(Path::new("")));
|
|
606
|
+
sync_parent(Path::new("backup")).unwrap();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
fn backup_into_missing_parent_records_provenance_and_verifies() {
|
|
611
|
+
let root = std::env::temp_dir().join(format!(
|
|
612
|
+
"feltdb-provenance-{}-{}",
|
|
613
|
+
std::process::id(),
|
|
614
|
+
rand::random::<u64>()
|
|
615
|
+
));
|
|
616
|
+
fs::create_dir_all(&root).unwrap();
|
|
617
|
+
fs::write(root.join("state.log"), b"state").unwrap();
|
|
618
|
+
let archive = root.join("does/not/exist/yet/archive");
|
|
619
|
+
let manifest = create_bundle_with_source(
|
|
620
|
+
&archive,
|
|
621
|
+
&[("state.log".into(), root.join("state.log"))],
|
|
622
|
+
&[],
|
|
623
|
+
BackupSource {
|
|
624
|
+
data: Some("/data/state.log".into()),
|
|
625
|
+
..BackupSource::default()
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
.unwrap();
|
|
629
|
+
let source = manifest.source.unwrap();
|
|
630
|
+
assert_eq!(source.data.as_deref(), Some("/data/state.log"));
|
|
631
|
+
assert_eq!(
|
|
632
|
+
source.state_sha256.as_deref(),
|
|
633
|
+
Some(manifest.entries[0].sha256.as_str())
|
|
634
|
+
);
|
|
635
|
+
let report = verification_report(&archive, Instant::now(), "verify");
|
|
636
|
+
assert_eq!(report.status, "ok");
|
|
637
|
+
assert_eq!(report.verification, "verified");
|
|
638
|
+
assert_eq!(report.bytes_written, 5);
|
|
639
|
+
assert_eq!(report.backup_id, backup_id(&archive).unwrap());
|
|
640
|
+
fs::remove_dir_all(root).unwrap();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
fn unrecorded_files_and_missing_manifests_fail_verification() {
|
|
645
|
+
let root = std::env::temp_dir().join(format!(
|
|
646
|
+
"feltdb-completeness-{}-{}",
|
|
647
|
+
std::process::id(),
|
|
648
|
+
rand::random::<u64>()
|
|
649
|
+
));
|
|
650
|
+
fs::create_dir_all(&root).unwrap();
|
|
651
|
+
fs::write(root.join("state.log"), b"state").unwrap();
|
|
652
|
+
let archive = root.join("archive");
|
|
653
|
+
create_bundle(&archive, &[("state.log".into(), root.join("state.log"))]).unwrap();
|
|
654
|
+
fs::write(archive.join("files/injected"), b"unrecorded").unwrap();
|
|
655
|
+
let report = verification_report(&archive, Instant::now(), "verify");
|
|
656
|
+
assert_eq!(report.status, "failed");
|
|
657
|
+
assert!(report.error.unwrap().contains("absent from its manifest"));
|
|
658
|
+
fs::remove_file(archive.join("files/injected")).unwrap();
|
|
659
|
+
fs::remove_file(archive.join("files/state.log")).unwrap();
|
|
660
|
+
assert_eq!(
|
|
661
|
+
verification_report(&archive, Instant::now(), "verify").status,
|
|
662
|
+
"failed"
|
|
663
|
+
);
|
|
664
|
+
fs::remove_file(archive.join("manifest.json")).unwrap();
|
|
665
|
+
assert_eq!(
|
|
666
|
+
verification_report(&archive, Instant::now(), "verify").verification,
|
|
667
|
+
"failed"
|
|
668
|
+
);
|
|
669
|
+
fs::remove_dir_all(root).unwrap();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
#[test]
|
|
673
|
+
fn manifests_written_before_provenance_still_verify() {
|
|
674
|
+
let root = std::env::temp_dir().join(format!(
|
|
675
|
+
"feltdb-legacy-manifest-{}-{}",
|
|
676
|
+
std::process::id(),
|
|
677
|
+
rand::random::<u64>()
|
|
678
|
+
));
|
|
679
|
+
fs::create_dir_all(root.join("archive/files")).unwrap();
|
|
680
|
+
fs::write(root.join("archive/files/state.log"), b"legacy").unwrap();
|
|
681
|
+
let digest = format!("{:x}", Sha256::digest(b"legacy"));
|
|
682
|
+
fs::write(
|
|
683
|
+
root.join("archive/manifest.json"),
|
|
684
|
+
format!(r#"{{"format":1,"created_ms":1,"entries":[{{"path":"files/state.log","bytes":6,"sha256":"{digest}"}}]}}"#),
|
|
685
|
+
)
|
|
686
|
+
.unwrap();
|
|
687
|
+
let manifest = verify_bundle(&root.join("archive")).unwrap();
|
|
688
|
+
assert!(manifest.source.is_none());
|
|
689
|
+
assert_eq!(
|
|
690
|
+
verification_report(&root.join("archive"), Instant::now(), "verify").source,
|
|
691
|
+
BackupSource::default()
|
|
692
|
+
);
|
|
693
|
+
fs::remove_dir_all(root).unwrap();
|
|
694
|
+
}
|
|
412
695
|
}
|