use std::path::{Path, PathBuf};
use crate::identity::{self, Identity};
#[derive(Debug)]
struct Entry<T> {
identity: Identity,
path: PathBuf,
held: T,
}
#[derive(Debug)]
pub struct Table<T> {
entries: Vec<Entry<T>>,
}
impl<T> Default for Table<T> {
fn default() -> Self {
Self {
entries: Vec::new(),
}
}
}
impl<T> Table<T> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.entries.iter().map(|e| &e.held)
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.entries.iter_mut().map(|e| &mut e.held)
}
pub fn insert(&mut self, container: &Path, held: T) -> std::io::Result<()> {
self.entries.push(Entry {
identity: identity::of(container)?,
path: std::fs::canonicalize(container)?,
held,
});
Ok(())
}
pub fn find_mut(&mut self, container: &Path) -> Option<&mut T> {
let identity = identity::of(container).ok();
let path = std::fs::canonicalize(container).ok();
self.entries
.iter_mut()
.find(|e| {
identity.as_ref() == Some(&e.identity) || path.as_deref() == Some(e.path.as_path())
})
.map(|e| &mut e.held)
}
pub fn refresh(&mut self, container: &Path) {
let Ok(now) = identity::of(container) else {
return;
};
if let Some(entry) = self.entries.iter_mut().find(|e| {
e.path == container || Some(&e.path) == std::fs::canonicalize(container).ok().as_ref()
}) {
entry.identity = now;
}
}
pub fn remove(&mut self, container: &Path) -> Option<T> {
let identity = identity::of(container).ok();
let path = std::fs::canonicalize(container).ok();
let at = self.entries.iter().position(|e| {
identity.as_ref() == Some(&e.identity) || path.as_deref() == Some(e.path.as_path())
})?;
Some(self.entries.remove(at).held)
}
pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ {
self.entries.drain(..).map(|e| e.held)
}
}
#[cfg(test)]
mod tests {
use super::Table;
use std::fs;
use std::path::{Path, PathBuf};
fn a_file(at: &Path, name: &str) -> PathBuf {
let p = at.join(name);
fs::write(&p, b"container").unwrap();
p
}
#[test]
fn a_container_finds_its_own_session() {
let tmp = tempfile::tempdir().unwrap();
let c = a_file(tmp.path(), "report.slpc");
let mut table = Table::new();
table.insert(&c, "session".to_string()).unwrap();
assert_eq!(table.find_mut(&c).map(|s| s.as_str()), Some("session"));
}
#[test]
fn another_container_finds_nothing() {
let tmp = tempfile::tempdir().unwrap();
let a = a_file(tmp.path(), "a.slpc");
let b = a_file(tmp.path(), "b.slpc");
let mut table = Table::new();
table.insert(&a, "a".to_string()).unwrap();
assert!(table.find_mut(&b).is_none());
}
#[cfg(unix)]
#[test]
fn a_second_hard_link_finds_the_same_session() {
let tmp = tempfile::tempdir().unwrap();
let a = a_file(tmp.path(), "a.slpc");
let b = tmp.path().join("b.slpc");
fs::hard_link(&a, &b).unwrap();
let mut table = Table::new();
table.insert(&a, "one session".to_string()).unwrap();
assert_eq!(table.find_mut(&b).map(|s| s.as_str()), Some("one session"));
}
#[cfg(unix)]
#[test]
fn a_symbolic_link_finds_the_same_session() {
let tmp = tempfile::tempdir().unwrap();
let a = a_file(tmp.path(), "a.slpc");
let link = tmp.path().join("link.slpc");
std::os::unix::fs::symlink(&a, &link).unwrap();
let mut table = Table::new();
table.insert(&a, "one session".to_string()).unwrap();
assert!(table.find_mut(&link).is_some());
}
#[test]
fn a_container_replaced_by_a_write_back_still_finds_its_session() {
let tmp = tempfile::tempdir().unwrap();
let c = a_file(tmp.path(), "report.slpc");
let mut table = Table::new();
table.insert(&c, "session".to_string()).unwrap();
let scratch = tmp.path().join("scratch");
fs::write(&scratch, b"repacked").unwrap();
fs::rename(&scratch, &c).unwrap();
assert_eq!(table.find_mut(&c).map(|s| s.as_str()), Some("session"));
}
#[cfg(unix)]
#[test]
fn the_other_hard_link_is_a_different_container_once_one_has_been_written_back() {
let tmp = tempfile::tempdir().unwrap();
let a = a_file(tmp.path(), "a.slpc");
let b = tmp.path().join("b.slpc");
fs::hard_link(&a, &b).unwrap();
let mut table = Table::new();
table.insert(&a, "session".to_string()).unwrap();
let scratch = tmp.path().join("scratch");
fs::write(&scratch, b"repacked").unwrap();
fs::rename(&scratch, &a).unwrap();
table.refresh(&a);
assert!(table.find_mut(&a).is_some());
assert!(
table.find_mut(&b).is_none(),
"the other link still holds the old contents and is its own container now"
);
}
#[test]
fn refreshing_keeps_the_identity_arm_working_after_a_save() {
let tmp = tempfile::tempdir().unwrap();
let c = a_file(tmp.path(), "report.slpc");
let mut table = Table::new();
table.insert(&c, "session".to_string()).unwrap();
let scratch = tmp.path().join("scratch");
fs::write(&scratch, b"repacked").unwrap();
fs::rename(&scratch, &c).unwrap();
table.refresh(&c);
#[cfg(unix)]
{
let link = tmp.path().join("link.slpc");
fs::hard_link(&c, &link).unwrap();
assert!(table.find_mut(&link).is_some());
}
assert!(table.find_mut(&c).is_some());
}
#[test]
fn a_container_that_is_not_there_matches_nothing_rather_than_failing() {
let tmp = tempfile::tempdir().unwrap();
let c = a_file(tmp.path(), "report.slpc");
let mut table = Table::new();
table.insert(&c, "session".to_string()).unwrap();
assert!(table.find_mut(&tmp.path().join("gone.slpc")).is_none());
}
#[test]
fn removing_hands_the_session_back_and_empties_the_table() {
let tmp = tempfile::tempdir().unwrap();
let c = a_file(tmp.path(), "report.slpc");
let mut table = Table::new();
table.insert(&c, "session".to_string()).unwrap();
assert_eq!(table.remove(&c), Some("session".to_string()));
assert!(table.is_empty());
assert!(table.remove(&c).is_none());
}
}