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
5.1 KiB175 linesraw
1use std::collections::HashMap;
2use std::io;
3use std::path::PathBuf;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use avril_caps::{Capabilities, PathTarget};
8use avril_kv::{Access, Store};
9
10fn denied(label: &str, write: bool) -> io::Error {
11    io::Error::new(
12        io::ErrorKind::PermissionDenied,
13        format!("{} {label}", if write { "write" } else { "read" }),
14    )
15}
16
17fn authorize(caps: &Capabilities, target: &PathTarget, label: &str, write: bool) -> io::Result<()> {
18    if write {
19        caps.check_write(target)
20    } else {
21        caps.check_read(target)
22    }
23    .map_err(|_| denied(label, write))
24}
25
26fn absolute(caps: &Capabilities, label: &str) -> io::Result<(PathBuf, PathTarget)> {
27    if label.is_empty() {
28        return Err(io::Error::new(
29            io::ErrorKind::NotFound,
30            "Empty kv database path",
31        ));
32    }
33    let path = caps.cwd().join(label);
34    // Resolve the existing prefix like ext:file does; a missing tail is
35    // expected when creating, so refusal to resolve only denies when the
36    // grant could not have covered the path anyway.
37    let target = match PathTarget::for_creation(&path) {
38        Ok(target) => target,
39        Err(_) if !caps.unrestricted_read() && !caps.unrestricted_write() => {
40            return Err(denied(label, false));
41        }
42        Err(error) => return Err(error),
43    };
44    Ok((path, target))
45}
46
47struct Auth {
48    caps: Arc<Capabilities>,
49    target: PathTarget,
50    label: String,
51}
52
53pub struct Entry {
54    state: Mutex<EntryState>,
55}
56
57struct EntryState {
58    store: Option<Store>,
59    auth: Auth,
60}
61
62pub struct Registry {
63    next: AtomicU64,
64    entries: Mutex<HashMap<u64, Arc<Entry>>>,
65}
66
67impl Default for Registry {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73fn closed_handle() -> io::Error {
74    io::Error::new(io::ErrorKind::NotConnected, "kv database handle is closed")
75}
76
77impl Registry {
78    pub fn new() -> Self {
79        Self {
80            next: AtomicU64::new(1),
81            entries: Mutex::new(HashMap::new()),
82        }
83    }
84
85    fn lock_entries(&self) -> MutexGuard<'_, HashMap<u64, Arc<Entry>>> {
86        self.entries
87            .lock()
88            .unwrap_or_else(|error| error.into_inner())
89    }
90
91    fn entry(&self, id: u64) -> io::Result<Arc<Entry>> {
92        self.lock_entries().get(&id).cloned().ok_or_else(|| {
93            io::Error::new(
94                io::ErrorKind::NotConnected,
95                "kv database handle is closed or unknown",
96            )
97        })
98    }
99
100    pub fn with_entry<T>(
101        &self,
102        id: u64,
103        write: bool,
104        work: impl FnOnce(&Store) -> io::Result<T>,
105    ) -> io::Result<T> {
106        let entry = self.entry(id)?;
107        let state = entry
108            .state
109            .lock()
110            .unwrap_or_else(|error| error.into_inner());
111        let Some(store) = state.store.as_ref() else {
112            return Err(closed_handle());
113        };
114        // Re-authorize against the live Capabilities on every operation so a
115        // pledge() narrowing applies to already-open handles. Writes need
116        // read access too: a write transaction reads internally.
117        let auth = &state.auth;
118        authorize(&auth.caps, &auth.target, &auth.label, false)?;
119        if write {
120            authorize(&auth.caps, &auth.target, &auth.label, true)?;
121        }
122        // The entry mutex stays held for the whole operation: close() takes
123        // the store under the same mutex, so nothing is in flight when the
124        // last Arc drops and the database file is physically released.
125        work(store)
126    }
127
128    pub fn open(
129        &self,
130        caps: &Arc<Capabilities>,
131        label: &str,
132        access: &str,
133        create: bool,
134    ) -> io::Result<u64> {
135        let access = Access::parse(access)?;
136        let (path, target) = absolute(caps, label)?;
137        authorize(caps, &target, label, false)?;
138        if access.writes() {
139            authorize(caps, &target, label, true)?;
140        }
141        let store = Store::open(&path, access, create)?;
142        let id = self.next.fetch_add(1, Ordering::Relaxed);
143        self.lock_entries().insert(
144            id,
145            Arc::new(Entry {
146                state: Mutex::new(EntryState {
147                    store: Some(store),
148                    auth: Auth {
149                        caps: caps.clone(),
150                        target,
151                        label: label.to_owned(),
152                    },
153                }),
154            }),
155        );
156        Ok(id)
157    }
158
159    // Idempotent: closing an unknown or already-closed id succeeds. Taking
160    // the store under the entry mutex physically releases the database:
161    // with_entry holds that mutex for its whole operation, so nothing is in
162    // flight when close returns.
163    pub fn close(&self, id: u64) -> io::Result<()> {
164        let entry = self.lock_entries().remove(&id);
165        if let Some(entry) = entry {
166            let mut state = entry
167                .state
168                .lock()
169                .unwrap_or_else(|error| error.into_inner());
170            state.store.take();
171            drop(state);
172        }
173        Ok(())
174    }
175}