1use time::OffsetDateTime;
6use uuid::Uuid;
7
8use super::{Caller, Store, StoreError, Stored, Tx, commit, version_row};
9use crate::audit::{Action, Actor, Entry};
10use crate::clock::from_nanos;
11use crate::ids::VersionId;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Legal {
16 pub stored: Stored,
17 pub deleted: bool,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct VersionInfo {
24 pub version: VersionId,
25 pub stored_at: OffsetDateTime,
26 pub hidden_at: Option<OffsetDateTime>,
28 pub current: bool,
30}
31
32enum Standing {
34 Purged,
35 Absent,
36 Live(VersionId),
37 Deleted,
38}
39
40fn standing(tx: &mut Tx, uuid: Uuid) -> Result<Standing, StoreError> {
41 if tx.tombstoned(uuid)? {
42 return Ok(Standing::Purged);
43 }
44 match tx.vcon_row(uuid)? {
45 None => Ok(Standing::Absent),
46 Some(row) if row.deleted_at.is_some() => Ok(Standing::Deleted),
47 Some(row) => row.current.map(Standing::Live).ok_or(StoreError::Damaged),
48 }
49}
50
51fn status<T>(answer: &Result<T, StoreError>, ok: u16) -> u16 {
53 match answer {
54 Ok(_) => ok,
55 Err(StoreError::Gone) => 410,
56 Err(StoreError::NotFound) => 404,
57 Err(StoreError::NotDeleted) => 409,
58 Err(_) => 500,
59 }
60}
61
62fn latest(tx: &mut Tx, uuid: Uuid) -> Result<VersionId, StoreError> {
65 tx.latest(uuid)?.ok_or(StoreError::Damaged)
66}
67
68impl Store {
69 pub fn legal_get(
75 &self,
76 who: &Caller,
77 uuid: Uuid,
78 version: Option<VersionId>,
79 ) -> Result<Legal, StoreError> {
80 let mut tx = self.catalog.immediate()?;
81 let answer = self.legal_read(&mut tx, uuid, version);
82 let entry = Entry {
83 actor: Actor::Key(who.key),
84 action: Action::LegalRead,
85 target: Some(uuid.to_string()),
86 version: answer.as_ref().ok().map(|read| read.stored.version),
87 status: status(&answer, 200),
88 };
89 self.record(&mut tx, &entry)?;
90 commit(tx)?;
91 answer
92 }
93
94 fn legal_read(
95 &self,
96 tx: &mut Tx,
97 uuid: Uuid,
98 asked: Option<VersionId>,
99 ) -> Result<Legal, StoreError> {
100 let (current, deleted) = match standing(tx, uuid)? {
101 Standing::Purged => return Err(StoreError::Gone),
102 Standing::Absent => return Err(StoreError::NotFound),
103 Standing::Live(current) => (Some(current), false),
104 Standing::Deleted => (None, true),
105 };
106 let version = match (asked, current) {
107 (Some(asked), _) if tx.has_version(asked, uuid)? => asked,
108 (Some(_), _) => return Err(StoreError::NotFound),
109 (None, Some(current)) => current,
110 (None, None) => latest(tx, uuid)?,
111 };
112 let row = version_row(tx, version)?;
113 let body = self.open_version(uuid, version, &row.wrapped)?;
114 Ok(Legal {
115 stored: Stored {
116 version,
117 form: row.form,
118 gzip: row.gzip,
119 body,
120 },
121 deleted,
122 })
123 }
124
125 pub fn legal_versions(&self, who: &Caller, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
128 let mut tx = self.catalog.immediate()?;
129 let answer = versions(&mut tx, uuid);
130 let entry = Entry {
131 actor: Actor::Key(who.key),
132 action: Action::LegalVersions,
133 target: Some(uuid.to_string()),
134 version: None,
135 status: status(&answer, 200),
136 };
137 self.record(&mut tx, &entry)?;
138 commit(tx)?;
139 answer
140 }
141
142 pub fn restore(&self, who: &Caller, uuid: Uuid) -> Result<VersionId, StoreError> {
147 let mut tx = self.catalog.immediate()?;
148 let answer = in_part(&mut tx, |tx| restored(tx, uuid))?;
151 let entry = Entry {
152 actor: Actor::Key(who.key),
153 action: Action::Restore,
154 target: Some(uuid.to_string()),
155 version: answer.as_ref().ok().copied(),
156 status: status(&answer, 200),
157 };
158 self.record(&mut tx, &entry)?;
159 commit(tx)?;
160 answer
161 }
162
163 pub fn set_hold(&self, who: &Caller, uuid: Uuid, on: bool) -> Result<(), StoreError> {
167 let mut tx = self.catalog.immediate()?;
168 let answer = in_part(&mut tx, |tx| held(tx, uuid, on))?;
169 let entry = Entry {
170 actor: Actor::Key(who.key),
171 action: if on { Action::Hold } else { Action::Release },
172 target: Some(uuid.to_string()),
173 version: None,
174 status: status(&answer, 204),
175 };
176 self.record(&mut tx, &entry)?;
177 commit(tx)?;
178 answer
179 }
180}
181
182fn in_part<T>(
186 tx: &mut Tx,
187 writes: impl FnOnce(&mut Tx) -> Result<T, StoreError>,
188) -> Result<Result<T, StoreError>, StoreError> {
189 tx.begin_part()?;
190 let answer = writes(tx);
191 if answer.is_ok() {
192 tx.release_part()?;
193 } else {
194 tx.rollback_part()?;
195 }
196 Ok(answer)
197}
198
199fn restored(tx: &mut Tx, uuid: Uuid) -> Result<VersionId, StoreError> {
200 match standing(tx, uuid)? {
201 Standing::Purged => return Err(StoreError::Gone),
202 Standing::Absent => return Err(StoreError::NotFound),
203 Standing::Live(_) => return Err(StoreError::NotDeleted),
204 Standing::Deleted => {}
205 }
206 let version = latest(tx, uuid)?;
207 tx.unhide(version)?;
208 tx.undelete(uuid, version)?;
209 Ok(version)
210}
211
212fn held(tx: &mut Tx, uuid: Uuid, on: bool) -> Result<(), StoreError> {
213 match standing(tx, uuid)? {
214 Standing::Purged => Err(StoreError::Gone),
215 Standing::Absent => Err(StoreError::NotFound),
216 Standing::Live(_) | Standing::Deleted => tx.set_hold(uuid, on),
217 }
218}
219
220fn versions(tx: &mut Tx, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
221 let current = match standing(tx, uuid)? {
222 Standing::Purged => return Err(StoreError::Gone),
223 Standing::Absent => return Err(StoreError::NotFound),
224 Standing::Live(current) => Some(current),
225 Standing::Deleted => None,
226 };
227 let time = |nanos: i64| from_nanos(nanos).ok_or(StoreError::Damaged);
228 tx.kept(uuid)?
229 .into_iter()
230 .map(|row| {
231 Ok(VersionInfo {
232 version: row.version,
233 stored_at: time(row.stored_at)?,
234 hidden_at: row.hidden_at.map(time).transpose()?,
235 current: Some(row.version) == current,
236 })
237 })
238 .collect()
239}
240
241#[cfg(test)]
242mod legal_tests {
243 use serde_json::Value;
244 use time::Duration;
245 use time::macros::datetime;
246 use uuid::Uuid;
247
248 use super::super::testing::vcon;
249 use super::super::tests::{Fixture, fixture};
250 use super::*;
251 use crate::clock::Clock;
252 use crate::fs::Fs;
253 use crate::ids::VersionId;
254 use crate::retention::Retention;
255 use crate::store::{Created, StoreError, Stored};
256
257 const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
258 const OTHER: &str = "0199e5c6-0d7a-7c11-8000-000000000001";
259
260 fn stored(f: &Fixture, uuid: &str, subject: &str) -> (VersionId, Vec<u8>) {
262 let (found, body) = vcon(uuid, subject);
263 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
264 panic!("a new vCon")
265 };
266 (version, body)
267 }
268
269 fn replaced(f: &Fixture, current: VersionId, subject: &str) -> (VersionId, Vec<u8>) {
271 let (found, body) = vcon(UUID, subject);
272 let version = f
273 .store
274 .replace(&f.caller, current, &found, &body)
275 .expect("replaced");
276 (version, body)
277 }
278
279 fn uuid() -> Uuid {
280 UUID.parse().expect("uuid")
281 }
282
283 fn later(f: &Fixture, by: Duration) {
284 f.clock.set(f.clock.now() + by);
285 }
286
287 fn last(f: &Fixture, action: &str) -> Value {
289 f.store
290 .records()
291 .into_iter()
292 .rfind(|r| r["action"] == action)
293 .expect("a record")
294 }
295
296 #[test]
297 fn a_deleted_vcon_reads_back_byte_for_byte_and_says_it_is_deleted() {
298 let f = fixture();
299 let (version, body) = stored(&f, UUID, "first");
300 f.store
301 .delete(&f.caller, uuid(), Retention::Years(7))
302 .expect("deleted");
303
304 let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
305
306 assert_eq!(
307 read,
308 Legal {
309 stored: Stored {
310 version,
311 form: vcon::Form::Unsigned,
312 gzip: false,
313 body,
314 },
315 deleted: true,
316 }
317 );
318 let record = last(&f, "legal_read");
319 assert_eq!(record["status"], 200);
320 assert_eq!(record["target"], UUID);
321 assert_eq!(record["version"], version.to_string());
322 }
323
324 #[test]
325 fn a_deleted_vcon_reads_its_latest_kept_version() {
326 let f = fixture();
327 let (first, _) = stored(&f, UUID, "first");
328 later(&f, Duration::hours(1));
329 let (second, body) = replaced(&f, first, "second");
330 f.store
331 .delete(&f.caller, uuid(), Retention::Years(7))
332 .expect("deleted");
333
334 let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
335
336 assert_eq!((read.stored.version, read.stored.body), (second, body));
337 }
338
339 #[test]
340 fn a_live_vcon_reads_its_current_version() {
341 let f = fixture();
342 let (first, _) = stored(&f, UUID, "first");
343 let (second, body) = replaced(&f, first, "second");
344
345 let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
346
347 assert_eq!((read.stored.version, read.stored.body), (second, body));
348 assert!(!read.deleted);
349 }
350
351 #[test]
352 fn a_named_kept_version_reads_back_until_it_is_purged() {
353 let f = fixture();
354 let (first, body) = stored(&f, UUID, "first");
355 replaced(&f, first, "second");
356
357 let read = f
358 .store
359 .legal_get(&f.caller, uuid(), Some(first))
360 .expect("read");
361 assert_eq!((read.stored.version, read.stored.body), (first, body));
362 assert!(!read.deleted, "the uuid is live");
363
364 f.store.purge_due(Retention::Days(0)).expect("purged");
365 assert_eq!(
366 f.store.legal_get(&f.caller, uuid(), Some(first)),
367 Err(StoreError::NotFound)
368 );
369 assert_eq!(last(&f, "legal_read")["status"], 404);
370 }
371
372 #[test]
373 fn a_version_of_another_uuid_is_not_found() {
374 let f = fixture();
375 stored(&f, UUID, "first");
376 let (theirs, _) = stored(&f, OTHER, "other");
377
378 assert_eq!(
379 f.store.legal_get(&f.caller, uuid(), Some(theirs)),
380 Err(StoreError::NotFound)
381 );
382 }
383
384 #[test]
385 fn a_purged_uuid_is_gone_and_one_never_stored_is_not_found() {
386 let f = fixture();
387 stored(&f, UUID, "first");
388 f.store
389 .delete(&f.caller, uuid(), Retention::Days(0))
390 .expect("deleted and purged");
391
392 assert_eq!(
393 f.store.legal_get(&f.caller, uuid(), None),
394 Err(StoreError::Gone)
395 );
396 assert_eq!(last(&f, "legal_read")["status"], 410);
397 assert_eq!(
398 f.store.legal_versions(&f.caller, uuid()),
399 Err(StoreError::Gone)
400 );
401 assert_eq!(last(&f, "legal_versions")["status"], 410);
402
403 let never: Uuid = OTHER.parse().expect("uuid");
404 assert_eq!(
405 f.store.legal_get(&f.caller, never, None),
406 Err(StoreError::NotFound)
407 );
408 assert_eq!(
409 f.store.legal_versions(&f.caller, never),
410 Err(StoreError::NotFound)
411 );
412 assert_eq!(last(&f, "legal_versions")["status"], 404);
413 }
414
415 #[test]
416 fn the_version_list_gives_every_kept_version_with_its_times() {
417 let f = fixture();
418 let (first, _) = stored(&f, UUID, "first");
419 f.clock.set(datetime!(2026-10-23 13:00 UTC));
420 let (second, _) = replaced(&f, first, "second");
421
422 let live = f.store.legal_versions(&f.caller, uuid()).expect("list");
423 assert_eq!(
424 live,
425 [
426 VersionInfo {
427 version: first,
428 stored_at: datetime!(2026-10-23 12:00 UTC),
429 hidden_at: Some(datetime!(2026-10-23 13:00 UTC)),
430 current: false,
431 },
432 VersionInfo {
433 version: second,
434 stored_at: datetime!(2026-10-23 13:00 UTC),
435 hidden_at: None,
436 current: true,
437 },
438 ]
439 );
440 let record = last(&f, "legal_versions");
441 assert_eq!(
442 (record["status"].clone(), record["target"].clone()),
443 (200.into(), UUID.into())
444 );
445
446 f.clock.set(datetime!(2026-10-23 14:00 UTC));
447 f.store
448 .delete(&f.caller, uuid(), Retention::Years(7))
449 .expect("deleted");
450 let deleted = f.store.legal_versions(&f.caller, uuid()).expect("list");
451 assert_eq!(deleted.len(), 2);
452 assert_eq!(deleted[1].hidden_at, Some(datetime!(2026-10-23 14:00 UTC)));
453 assert!(deleted.iter().all(|v| !v.current), "nothing is current");
454 }
455
456 #[test]
457 fn a_damaged_kept_version_is_a_500_never_a_body() {
458 let f = fixture();
459 let (version, _) = stored(&f, UUID, "first");
460 f.store
461 .delete(&f.caller, uuid(), Retention::Years(7))
462 .expect("deleted");
463 let file = f.store.objects_dir.join(version.to_string());
464 let mut blob = f.fs.read(&file).expect("file");
465 let last_byte = blob.len() - 1;
466 blob[last_byte] ^= 1;
467 f.fs.remove(&file).expect("remove");
468 f.fs.write_new(&file, &blob).expect("write");
469
470 assert_eq!(
471 f.store.legal_get(&f.caller, uuid(), None),
472 Err(StoreError::Damaged)
473 );
474 assert_eq!(last(&f, "legal_read")["status"], 500);
475 }
476
477 #[test]
478 fn a_restored_vcon_is_current_again_and_a_sweep_leaves_it() {
479 let f = fixture();
480 let (first, _) = stored(&f, UUID, "first");
481 later(&f, Duration::hours(1));
482 let (second, body) = replaced(&f, first, "second");
483 f.store
484 .delete(&f.caller, uuid(), Retention::Years(7))
485 .expect("deleted");
486 later(&f, Duration::days(2));
487
488 assert_eq!(f.store.restore(&f.caller, uuid()), Ok(second));
489 let record = last(&f, "restore");
490 assert_eq!(record["status"], 200);
491 assert_eq!(record["version"], second.to_string());
492
493 let read = f.store.get(&f.caller, uuid()).expect("live again");
494 assert_eq!((read.version, read.body), (second, body));
495 f.store.purge_due(Retention::Days(1)).expect("swept");
496 assert_eq!(
497 f.store.get(&f.caller, uuid()).map(|s| s.version),
498 Ok(second),
499 "the sweep leaves it current"
500 );
501 let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
502 assert_eq!(
503 kept.iter().map(|v| v.version).collect::<Vec<_>>(),
504 [second],
505 "the older hidden version was due and is gone"
506 );
507 }
508
509 #[test]
510 fn restore_refuses_a_live_a_purged_and_a_never_stored_uuid() {
511 let f = fixture();
512 stored(&f, UUID, "first");
513 assert_eq!(
514 f.store.restore(&f.caller, uuid()),
515 Err(StoreError::NotDeleted)
516 );
517 assert_eq!(last(&f, "restore")["status"], 409);
518
519 f.store
520 .delete(&f.caller, uuid(), Retention::Days(0))
521 .expect("deleted and purged");
522 assert_eq!(f.store.restore(&f.caller, uuid()), Err(StoreError::Gone));
523 assert_eq!(last(&f, "restore")["status"], 410);
524
525 let never: Uuid = OTHER.parse().expect("uuid");
526 assert_eq!(f.store.restore(&f.caller, never), Err(StoreError::NotFound));
527 assert_eq!(last(&f, "restore")["status"], 404);
528 }
529
530 #[test]
531 fn a_hold_blocks_the_purge_until_it_is_released() {
532 let f = fixture();
533 stored(&f, UUID, "first");
534 assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
535 assert_eq!(last(&f, "hold")["status"], 204);
536 f.store
537 .delete(&f.caller, uuid(), Retention::Days(0))
538 .expect("deleted, but held");
539 f.store.purge_due(Retention::Days(0)).expect("swept");
540 assert!(
541 f.store.legal_get(&f.caller, uuid(), None).is_ok(),
542 "held, so kept"
543 );
544
545 assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
546 f.store.purge_due(Retention::Days(0)).expect("swept");
547 assert_eq!(
548 f.store.legal_get(&f.caller, uuid(), None),
549 Err(StoreError::Gone),
550 "released, so purged"
551 );
552 }
553
554 #[test]
556 fn a_release_is_recorded_as_release_not_as_hold() {
557 let f = fixture();
558 stored(&f, UUID, "first");
559 assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
560 assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
561 let actions: Vec<_> = f
562 .store
563 .records()
564 .into_iter()
565 .filter(|r| r["target"] == UUID)
566 .map(|r| r["action"].as_str().expect("an action").to_owned())
567 .collect();
568 assert_eq!(actions, ["create", "hold", "release"]);
569 assert_eq!(last(&f, "release")["status"], 204);
570 let never: Uuid = OTHER.parse().expect("uuid");
571 assert_eq!(
572 f.store.set_hold(&f.caller, never, false),
573 Err(StoreError::NotFound)
574 );
575 assert_eq!(last(&f, "release")["status"], 404, "a refused release too");
576 }
577
578 #[test]
579 fn a_hold_refuses_a_purged_and_a_never_stored_uuid() {
580 let f = fixture();
581 stored(&f, UUID, "first");
582 f.store
583 .delete(&f.caller, uuid(), Retention::Days(0))
584 .expect("deleted and purged");
585 assert_eq!(
586 f.store.set_hold(&f.caller, uuid(), true),
587 Err(StoreError::Gone)
588 );
589 assert_eq!(last(&f, "hold")["status"], 410);
590 let never: Uuid = OTHER.parse().expect("uuid");
591 assert_eq!(
592 f.store.set_hold(&f.caller, never, true),
593 Err(StoreError::NotFound)
594 );
595 assert_eq!(last(&f, "hold")["status"], 404);
596 }
597
598 #[test]
599 fn a_restore_that_fails_partway_changes_nothing_and_is_recorded() {
600 let f = fixture();
601 stored(&f, UUID, "first");
602 f.store
603 .delete(&f.caller, uuid(), Retention::Years(7))
604 .expect("deleted");
605 f.store
606 .conn()
607 .execute_batch(
608 "CREATE TEMP TRIGGER no_restore BEFORE UPDATE OF current ON vcon
609 BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
610 )
611 .expect("trigger");
612
613 assert!(f.store.restore(&f.caller, uuid()).is_err());
614
615 assert_eq!(last(&f, "restore")["status"], 500);
616 let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
617 assert!(
618 kept.iter().all(|v| v.hidden_at.is_some()),
619 "the version written first is hidden again"
620 );
621 }
622}