Skip to main content

superfluid_daemon/
store_id.rs

1//! A session store's persisted identity; see [`StoreId`].
2
3use std::path::Path;
4
5use crate::DaemonError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8pub struct StoreId(pub [u8; 16]);
9
10impl StoreId {
11    pub fn mint() -> StoreId {
12        let mut b = [0u8; 16];
13        // SAFETY: `b` is a valid, writable 16-byte buffer; getentropy
14        // writes at most `len` (16 <= 256, its per-call limit) bytes.
15        let rc = unsafe { libc::getentropy(b.as_mut_ptr().cast(), b.len()) };
16        if rc != 0 {
17            static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18            let t = std::time::SystemTime::now()
19                .duration_since(std::time::UNIX_EPOCH)
20                .map(|d| d.as_nanos())
21                .unwrap_or(0);
22            let seed = [
23                t.to_le_bytes().as_slice(),
24                &std::process::id().to_le_bytes(),
25                &N.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
26                    .to_le_bytes(),
27            ]
28            .concat();
29            b = xxhash_rust::xxh3::xxh3_128(&seed).to_le_bytes();
30        }
31        StoreId(b)
32    }
33
34    pub fn to_hex(&self) -> String {
35        self.0.iter().map(|b| format!("{b:02x}")).collect()
36    }
37
38    pub fn from_hex(s: &str) -> Option<StoreId> {
39        let s = s.trim();
40        if s.len() != 32 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
41            return None;
42        }
43        let mut b = [0u8; 16];
44        for (i, out) in b.iter_mut().enumerate() {
45            *out = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?;
46        }
47        Some(StoreId(b))
48    }
49
50    pub fn to_file_line(&self) -> String {
51        format!("{} {:016x}\n", self.to_hex(), Self::checksum(&self.0))
52    }
53
54    fn checksum(b: &[u8; 16]) -> u64 {
55        xxhash_rust::xxh3::xxh3_64_with_seed(b, 0x5354_4f52_4549_4421)
56    }
57
58    pub fn from_file_line(text: &str) -> Option<StoreId> {
59        let mut parts = text.split_whitespace();
60        let (id, sum) = (parts.next()?, parts.next()?);
61        if parts.next().is_some() {
62            return None;
63        }
64        let id = StoreId::from_hex(id)?;
65        let sum = u64::from_str_radix(sum, 16)
66            .ok()
67            .filter(|_| sum.len() == 16)?;
68        (Self::checksum(&id.0) == sum).then_some(id)
69    }
70
71    pub fn load_or_mint(path: &Path) -> Result<StoreId, DaemonError> {
72        let lock = StoreId::lock(path)?;
73        Self::open(path, false, &lock)
74    }
75
76    pub fn lock(path: &Path) -> Result<StoreIdLock, DaemonError> {
77        Ok(StoreIdLock::acquire(path, libc::LOCK_EX)?.expect("a blocking flock returns held"))
78    }
79
80    pub fn try_lock(path: &Path) -> Result<Option<StoreIdLock>, DaemonError> {
81        StoreIdLock::acquire(path, libc::LOCK_EX | libc::LOCK_NB)
82    }
83
84    pub fn open(path: &Path, fresh_wal: bool, _lock: &StoreIdLock) -> Result<StoreId, DaemonError> {
85        let dir = path
86            .parent()
87            .filter(|d| !d.as_os_str().is_empty())
88            .unwrap_or(Path::new("."));
89        let existing = Self::load(path)?;
90        if let (false, Some(id)) = (fresh_wal, existing) {
91            sync_dir(dir)?;
92            return Ok(id);
93        }
94        let id = StoreId::mint();
95        let name = path
96            .file_name()
97            .and_then(|n| n.to_str())
98            .unwrap_or("store-id");
99        let tmp = TempFile::write(dir, name, &id)?;
100        let id = if existing.is_some() {
101            std::fs::rename(&tmp.path, path)?;
102            id
103        } else {
104            match std::fs::hard_link(&tmp.path, path) {
105                Ok(()) => id,
106                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Self::load(path)?
107                    .ok_or(DaemonError::Protocol(
108                        "store id vanished while being published",
109                    ))?,
110                Err(e) => return Err(e.into()),
111            }
112        };
113        drop(tmp);
114        sync_dir(dir)?;
115        Ok(id)
116    }
117
118    fn load(path: &Path) -> Result<Option<StoreId>, DaemonError> {
119        match std::fs::read_to_string(path) {
120            Ok(text) => StoreId::from_file_line(&text)
121                .map(Some)
122                .ok_or(DaemonError::Config(
123                    "the session store's id file is corrupt; it only names the store's \
124                     trace ids — delete it to mint a new one (exported trace ids change)",
125                )),
126            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
127            Err(e) => Err(e.into()),
128        }
129    }
130}
131
132pub struct StoreIdLock {
133    _file: std::fs::File,
134}
135
136impl StoreIdLock {
137    fn acquire(id_path: &Path, op: libc::c_int) -> Result<Option<StoreIdLock>, DaemonError> {
138        use std::os::fd::AsRawFd;
139        let mut name = id_path
140            .file_name()
141            .map(|n| n.to_os_string())
142            .unwrap_or_default();
143        name.push(".lock");
144        let file = std::fs::OpenOptions::new()
145            .read(true)
146            .write(true)
147            .create(true)
148            .truncate(false)
149            .open(id_path.with_file_name(name))?;
150        loop {
151            // SAFETY: `file` is an open descriptor owned by this function
152            // for the duration of the call; flock only takes its number.
153            let rc = unsafe { libc::flock(file.as_raw_fd(), op) };
154            if rc == 0 {
155                return Ok(Some(StoreIdLock { _file: file }));
156            }
157            let err = std::io::Error::last_os_error();
158            match err.kind() {
159                std::io::ErrorKind::Interrupted => {}
160                std::io::ErrorKind::WouldBlock => return Ok(None),
161                _ => return Err(err.into()),
162            }
163        }
164    }
165}
166
167fn sync_dir(dir: &Path) -> Result<(), DaemonError> {
168    std::fs::File::open(dir)?.sync_all()?;
169    Ok(())
170}
171
172struct TempFile {
173    path: std::path::PathBuf,
174}
175
176impl TempFile {
177    fn write(dir: &Path, name: &str, id: &StoreId) -> Result<TempFile, DaemonError> {
178        use std::io::Write;
179        let path = dir.join(format!(
180            ".{name}.tmp.{}.{}",
181            std::process::id(),
182            id.to_hex()
183        ));
184        let mut f = std::fs::OpenOptions::new()
185            .write(true)
186            .create_new(true)
187            .open(&path)?;
188        let tmp = TempFile { path };
189        f.write_all(id.to_file_line().as_bytes())?;
190        f.sync_all()?;
191        Ok(tmp)
192    }
193}
194
195impl Drop for TempFile {
196    fn drop(&mut self) {
197        let _ = std::fs::remove_file(&self.path);
198    }
199}
200
201pub fn store_id_path(wal: &Path) -> std::path::PathBuf {
202    let mut name = wal
203        .file_name()
204        .map(|n| n.to_os_string())
205        .unwrap_or_default();
206    name.push(".store-id");
207    wal.with_file_name(name)
208}