char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somadd `avril:kv` module78f4748

main
7.7 KiB237 linesraw
1use std::io;
2use std::sync::Arc;
3
4use avril_caps::{Capabilities, Category, Initial};
5use avril_kv::{BatchOp, Decision, StoredEntry};
6
7use super::registry::Registry;
8
9// The ops' shapes: one re-authorized store operation per call.
10trait Shorthand {
11    fn get(&self, db: u64, keyspace: &str, key: &[u8]) -> io::Result<Option<StoredEntry>>;
12    fn set(&self, db: u64, keyspace: &str, key: &[u8], value: &[u8]) -> io::Result<String>;
13    fn list_keyspaces(&self, db: u64) -> io::Result<Vec<String>>;
14    fn atomic_commit(&self, db: u64, ops: Vec<BatchOp>) -> io::Result<(bool, Option<String>)>;
15}
16
17impl Shorthand for Registry {
18    fn get(&self, db: u64, keyspace: &str, key: &[u8]) -> io::Result<Option<StoredEntry>> {
19        self.with_entry(db, false, |store| store.read_tx(|tx| tx.get(keyspace, key)))
20    }
21
22    fn set(&self, db: u64, keyspace: &str, key: &[u8], value: &[u8]) -> io::Result<String> {
23        self.with_entry(db, true, |store| {
24            store.write_tx(|tx| tx.put(keyspace, key, value).map(Decision::Commit))
25        })
26    }
27
28    fn list_keyspaces(&self, db: u64) -> io::Result<Vec<String>> {
29        self.with_entry(db, false, |store| store.read_tx(|tx| tx.list_keyspaces()))
30    }
31
32    fn atomic_commit(&self, db: u64, ops: Vec<BatchOp>) -> io::Result<(bool, Option<String>)> {
33        self.with_entry(db, true, |store| store.atomic_commit(&ops))
34    }
35}
36
37fn caps(dir: &std::path::Path) -> Arc<Capabilities> {
38    Capabilities::new(Initial::new(false), dir.to_owned())
39}
40
41fn open_shared(registry: &Registry, caps: &Arc<Capabilities>, name: &str) -> u64 {
42    registry.open(caps, name, "shared", true).unwrap()
43}
44
45fn set_op(keyspace: &str, key: &[u8], value: &[u8]) -> BatchOp {
46    BatchOp::Set {
47        keyspace: keyspace.into(),
48        key: key.to_vec(),
49        value: value.to_vec(),
50    }
51}
52
53fn get<'a>(
54    registry: &Registry,
55    db: u64,
56    keyspace: &'a str,
57    key: &'a [u8],
58) -> Option<(Vec<u8>, String)> {
59    registry.get(db, keyspace, key).unwrap()
60}
61
62#[test]
63fn atomic_commit_spans_keyspaces() {
64    let root = tempfile::tempdir().unwrap();
65    let registry = Registry::new();
66    let db = open_shared(&registry, &caps(root.path()), "kv.redb");
67    let (ok, stamp) = registry
68        .atomic_commit(
69            db,
70            vec![set_op("a", b"k1", b"v1"), set_op("b", b"k2", b"v2")],
71        )
72        .unwrap();
73    assert!(ok);
74    let stamp = stamp.unwrap();
75    assert_eq!(
76        get(&registry, db, "a", b"k1").unwrap(),
77        (b"v1".to_vec(), stamp.clone())
78    );
79    assert_eq!(
80        get(&registry, db, "b", b"k2").unwrap(),
81        (b"v2".to_vec(), stamp)
82    );
83}
84
85#[test]
86fn atomic_check_failure_writes_nothing() {
87    let root = tempfile::tempdir().unwrap();
88    let registry = Registry::new();
89    let db = open_shared(&registry, &caps(root.path()), "kv.redb");
90    let stamp = registry.set(db, "a", b"k", b"v").unwrap();
91    let check = BatchOp::Check {
92        keyspace: "a".into(),
93        key: b"k".to_vec(),
94        expected: None,
95    };
96    let (ok, commit) = registry
97        .atomic_commit(
98            db,
99            vec![check, set_op("a", b"k", b"v2"), set_op("b", b"k", b"w")],
100        )
101        .unwrap();
102    assert!(!ok);
103    assert_eq!(commit, None);
104    assert_eq!(
105        get(&registry, db, "a", b"k").unwrap(),
106        (b"v".to_vec(), stamp)
107    );
108    assert_eq!(get(&registry, db, "b", b"k"), None);
109}
110
111#[test]
112fn closed_and_unknown_handles() {
113    let root = tempfile::tempdir().unwrap();
114    let registry = Registry::new();
115    let db = open_shared(&registry, &caps(root.path()), "kv.redb");
116    registry.close(db).unwrap();
117    // Close is idempotent.
118    registry.close(db).unwrap();
119    registry.close(999_999).unwrap();
120    for error in [
121        registry.get(db, "ks", b"k").unwrap_err(),
122        registry.set(db, "ks", b"k", b"v").map(|_| ()).unwrap_err(),
123        registry.list_keyspaces(db).map(|_| ()).unwrap_err(),
124        registry
125            .atomic_commit(db, vec![set_op("ks", b"k", b"v")])
126            .map(|_| ())
127            .unwrap_err(),
128    ] {
129        assert_eq!(error.kind(), std::io::ErrorKind::NotConnected);
130    }
131    assert_eq!(
132        registry.get(999_999, "ks", b"k").unwrap_err().kind(),
133        std::io::ErrorKind::NotConnected
134    );
135}
136
137#[test]
138fn close_releases_the_database_file() {
139    let root = tempfile::tempdir().unwrap();
140    let registry = Registry::new();
141    let db = registry
142        .open(&caps(root.path()), "kv.redb", "exclusive", true)
143        .unwrap();
144    registry.set(db, "ks", b"k", b"v").unwrap();
145    registry.close(db).unwrap();
146    // Reopening exclusively works only if close physically released the file.
147    let db = registry
148        .open(&caps(root.path()), "kv.redb", "exclusive", false)
149        .unwrap();
150    registry.set(db, "ks", b"k", b"v2").unwrap();
151    assert_eq!(get(&registry, db, "ks", b"k").unwrap().0, b"v2");
152    registry.close(db).unwrap();
153}
154
155#[test]
156fn capability_denial() {
157    let root = tempfile::tempdir().unwrap();
158    let locked = Capabilities::new(Initial::new(true), root.path().to_owned());
159    let registry = Registry::new();
160    let error = registry
161        .open(&locked, "kv.redb", "shared", true)
162        .unwrap_err();
163    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
164    assert!(!root.path().join("kv.redb").exists());
165}
166
167#[test]
168fn pledge_narrowing_applies_to_open_handles() {
169    let root = tempfile::tempdir().unwrap();
170    let live = caps(root.path());
171    let registry = Registry::new();
172    let db = open_shared(&registry, &live, "kv.redb");
173    registry.set(db, "ks", b"k", b"v").unwrap();
174    assert!(registry.get(db, "ks", b"k").unwrap().is_some());
175    live.pledge(vec![
176        (Category::Read, Some(vec![]), vec![]),
177        (Category::Write, Some(vec![]), vec![]),
178    ])
179    .unwrap();
180    assert_eq!(
181        registry.get(db, "ks", b"k").unwrap_err().kind(),
182        std::io::ErrorKind::PermissionDenied
183    );
184    assert_eq!(
185        registry.set(db, "ks", b"k", b"v2").unwrap_err().kind(),
186        std::io::ErrorKind::PermissionDenied
187    );
188    assert_eq!(
189        registry.list_keyspaces(db).unwrap_err().kind(),
190        std::io::ErrorKind::PermissionDenied
191    );
192    assert_eq!(
193        registry
194            .atomic_commit(db, vec![set_op("ks", b"k", b"v3")])
195            .unwrap_err()
196            .kind(),
197        std::io::ErrorKind::PermissionDenied
198    );
199}
200
201#[test]
202fn two_read_only_handles_coexist_on_the_same_database() {
203    let root = tempfile::tempdir().unwrap();
204    let registry = Registry::new();
205    let db = open_shared(&registry, &caps(root.path()), "kv.redb");
206    registry.set(db, "ks", b"k", b"v").unwrap();
207    registry.close(db).unwrap();
208    let first = registry
209        .open(&caps(root.path()), "kv.redb", "read-only", false)
210        .unwrap();
211    let second = registry
212        .open(&caps(root.path()), "kv.redb", "read-only", false)
213        .unwrap();
214    assert_eq!(get(&registry, first, "ks", b"k").unwrap().0, b"v");
215    assert_eq!(get(&registry, second, "ks", b"k").unwrap().0, b"v");
216    registry.close(first).unwrap();
217    registry.close(second).unwrap();
218}
219
220#[test]
221fn read_only_handle_on_a_missing_file_does_not_block_exclusive_create() {
222    let root = tempfile::tempdir().unwrap();
223    let registry = Registry::new();
224    let read = registry
225        .open(&caps(root.path()), "kv.redb", "read-only", false)
226        .unwrap();
227    assert!(!root.path().join("kv.redb").exists());
228    let write = registry
229        .open(&caps(root.path()), "kv.redb", "exclusive", true)
230        .unwrap();
231    registry.set(write, "ks", b"k", b"v").unwrap();
232    // The read-only handle snapshotted the missing file at open; it still
233    // reads empty rather than observing the later creation.
234    assert_eq!(registry.get(read, "ks", b"k").unwrap(), None);
235    registry.close(read).unwrap();
236    registry.close(write).unwrap();
237}