1use std::path::{Path, PathBuf};
33
34use crate::identity::{self, Identity};
35
36#[derive(Debug)]
38struct Entry<T> {
39 identity: Identity,
40 path: PathBuf,
41 held: T,
42}
43
44#[derive(Debug)]
46pub struct Table<T> {
47 entries: Vec<Entry<T>>,
48}
49
50impl<T> Default for Table<T> {
51 fn default() -> Self {
52 Self {
53 entries: Vec::new(),
54 }
55 }
56}
57
58impl<T> Table<T> {
59 #[must_use]
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 #[must_use]
67 pub fn len(&self) -> usize {
68 self.entries.len()
69 }
70
71 #[must_use]
74 pub fn is_empty(&self) -> bool {
75 self.entries.is_empty()
76 }
77
78 pub fn iter(&self) -> impl Iterator<Item = &T> {
80 self.entries.iter().map(|e| &e.held)
81 }
82
83 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
85 self.entries.iter_mut().map(|e| &mut e.held)
86 }
87
88 pub fn insert(&mut self, container: &Path, held: T) -> std::io::Result<()> {
94 self.entries.push(Entry {
95 identity: identity::of(container)?,
96 path: std::fs::canonicalize(container)?,
97 held,
98 });
99 Ok(())
100 }
101
102 pub fn find_mut(&mut self, container: &Path) -> Option<&mut T> {
108 let identity = identity::of(container).ok();
109 let path = std::fs::canonicalize(container).ok();
110 self.entries
111 .iter_mut()
112 .find(|e| {
113 identity.as_ref() == Some(&e.identity) || path.as_deref() == Some(e.path.as_path())
114 })
115 .map(|e| &mut e.held)
116 }
117
118 pub fn refresh(&mut self, container: &Path) {
129 let Ok(now) = identity::of(container) else {
130 return;
131 };
132 if let Some(entry) = self.entries.iter_mut().find(|e| {
133 e.path == container || Some(&e.path) == std::fs::canonicalize(container).ok().as_ref()
134 }) {
135 entry.identity = now;
136 }
137 }
138
139 pub fn remove(&mut self, container: &Path) -> Option<T> {
141 let identity = identity::of(container).ok();
142 let path = std::fs::canonicalize(container).ok();
143 let at = self.entries.iter().position(|e| {
144 identity.as_ref() == Some(&e.identity) || path.as_deref() == Some(e.path.as_path())
145 })?;
146 Some(self.entries.remove(at).held)
147 }
148
149 pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ {
151 self.entries.drain(..).map(|e| e.held)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::Table;
158 use std::fs;
159 use std::path::{Path, PathBuf};
160
161 fn a_file(at: &Path, name: &str) -> PathBuf {
162 let p = at.join(name);
163 fs::write(&p, b"container").unwrap();
164 p
165 }
166
167 #[test]
168 fn a_container_finds_its_own_session() {
169 let tmp = tempfile::tempdir().unwrap();
170 let c = a_file(tmp.path(), "report.slpc");
171 let mut table = Table::new();
172 table.insert(&c, "session".to_string()).unwrap();
173 assert_eq!(table.find_mut(&c).map(|s| s.as_str()), Some("session"));
174 }
175
176 #[test]
177 fn another_container_finds_nothing() {
178 let tmp = tempfile::tempdir().unwrap();
179 let a = a_file(tmp.path(), "a.slpc");
180 let b = a_file(tmp.path(), "b.slpc");
181 let mut table = Table::new();
182 table.insert(&a, "a".to_string()).unwrap();
183 assert!(table.find_mut(&b).is_none());
184 }
185
186 #[cfg(unix)]
187 #[test]
188 fn a_second_hard_link_finds_the_same_session() {
189 let tmp = tempfile::tempdir().unwrap();
192 let a = a_file(tmp.path(), "a.slpc");
193 let b = tmp.path().join("b.slpc");
194 fs::hard_link(&a, &b).unwrap();
195
196 let mut table = Table::new();
197 table.insert(&a, "one session".to_string()).unwrap();
198 assert_eq!(table.find_mut(&b).map(|s| s.as_str()), Some("one session"));
199 }
200
201 #[cfg(unix)]
202 #[test]
203 fn a_symbolic_link_finds_the_same_session() {
204 let tmp = tempfile::tempdir().unwrap();
205 let a = a_file(tmp.path(), "a.slpc");
206 let link = tmp.path().join("link.slpc");
207 std::os::unix::fs::symlink(&a, &link).unwrap();
208
209 let mut table = Table::new();
210 table.insert(&a, "one session".to_string()).unwrap();
211 assert!(table.find_mut(&link).is_some());
212 }
213
214 #[test]
215 fn a_container_replaced_by_a_write_back_still_finds_its_session() {
216 let tmp = tempfile::tempdir().unwrap();
222 let c = a_file(tmp.path(), "report.slpc");
223 let mut table = Table::new();
224 table.insert(&c, "session".to_string()).unwrap();
225
226 let scratch = tmp.path().join("scratch");
227 fs::write(&scratch, b"repacked").unwrap();
228 fs::rename(&scratch, &c).unwrap();
229
230 assert_eq!(table.find_mut(&c).map(|s| s.as_str()), Some("session"));
231 }
232
233 #[cfg(unix)]
234 #[test]
235 fn the_other_hard_link_is_a_different_container_once_one_has_been_written_back() {
236 let tmp = tempfile::tempdir().unwrap();
242 let a = a_file(tmp.path(), "a.slpc");
243 let b = tmp.path().join("b.slpc");
244 fs::hard_link(&a, &b).unwrap();
245
246 let mut table = Table::new();
247 table.insert(&a, "session".to_string()).unwrap();
248
249 let scratch = tmp.path().join("scratch");
250 fs::write(&scratch, b"repacked").unwrap();
251 fs::rename(&scratch, &a).unwrap();
252 table.refresh(&a);
253
254 assert!(table.find_mut(&a).is_some());
255 assert!(
256 table.find_mut(&b).is_none(),
257 "the other link still holds the old contents and is its own container now"
258 );
259 }
260
261 #[test]
262 fn refreshing_keeps_the_identity_arm_working_after_a_save() {
263 let tmp = tempfile::tempdir().unwrap();
264 let c = a_file(tmp.path(), "report.slpc");
265 let mut table = Table::new();
266 table.insert(&c, "session".to_string()).unwrap();
267
268 let scratch = tmp.path().join("scratch");
269 fs::write(&scratch, b"repacked").unwrap();
270 fs::rename(&scratch, &c).unwrap();
271 table.refresh(&c);
272
273 #[cfg(unix)]
276 {
277 let link = tmp.path().join("link.slpc");
278 fs::hard_link(&c, &link).unwrap();
279 assert!(table.find_mut(&link).is_some());
280 }
281 assert!(table.find_mut(&c).is_some());
282 }
283
284 #[test]
285 fn a_container_that_is_not_there_matches_nothing_rather_than_failing() {
286 let tmp = tempfile::tempdir().unwrap();
287 let c = a_file(tmp.path(), "report.slpc");
288 let mut table = Table::new();
289 table.insert(&c, "session".to_string()).unwrap();
290 assert!(table.find_mut(&tmp.path().join("gone.slpc")).is_none());
291 }
292
293 #[test]
294 fn removing_hands_the_session_back_and_empties_the_table() {
295 let tmp = tempfile::tempdir().unwrap();
296 let c = a_file(tmp.path(), "report.slpc");
297 let mut table = Table::new();
298 table.insert(&c, "session".to_string()).unwrap();
299 assert_eq!(table.remove(&c), Some("session".to_string()));
300 assert!(table.is_empty());
301 assert!(table.remove(&c).is_none());
302 }
303}