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