use std::fmt;
use std::fs::File;
use slpc::Destination;
use crate::session::Session;
#[derive(Debug)]
pub enum Error {
Content(std::io::Error),
Container(std::io::Error),
ContainerChanged {
recorded: String,
found: String,
},
Repack(slpc::Error),
WouldNotBeConformant(String),
Swap(slpc::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Content(e) => write!(f, "the edited content file could not be read: {e}"),
Self::Container(e) => write!(f, "the container could not be opened: {e}"),
Self::ContainerChanged { recorded, found } => write!(
f,
"the container now holds {found} rather than {recorded}, so this is not the \
container this session was opened against. Nothing was changed."
),
Self::Repack(e) => write!(f, "the container could not be rebuilt: {e}"),
Self::WouldNotBeConformant(v) => write!(
f,
"the container this would have written is {v}. Nothing was changed."
),
Self::Swap(e) => write!(
f,
"the rebuilt container could not replace the original: {e}"
),
}
}
}
impl std::error::Error for Error {}
pub fn write_back(session: &mut Session) -> Result<(), Error> {
let container = session.record().container.clone();
let content_path = session.content_path();
let edited = File::open(&content_path).map_err(Error::Content)?;
let found = slpc::Container::open(&container)
.map_err(|e| match e {
slpc::Error::Io(e) => Error::Container(e),
other => Error::Repack(other),
})?
.content_name()
.to_string();
if found != session.record().content_name {
return Err(Error::ContainerChanged {
recorded: session.record().content_name.clone(),
found,
});
}
let source = File::open(&container).map_err(Error::Container)?;
let mut out = Destination::in_place(&container).map_err(Error::Swap)?;
slpc::Repack::new(source)
.content(&session.record().content_name, edited)
.write(out.writer())
.map_err(Error::Repack)?;
let verdict = slpc::validate(out.written().map_err(Error::Repack)?).map_err(Error::Repack)?;
if !verdict.is_conformant() {
return Err(Error::WouldNotBeConformant(verdict.to_string()));
}
out.commit().map_err(Error::Swap)?;
if let Ok(repacked) = slpc::Container::open(&container) {
if let Ok(crc) = repacked.content_crc() {
let _ = session.note_agreement(crc);
}
}
session.note_write_back().map_err(Error::Content)
}
#[cfg(test)]
mod tests {
use super::{write_back, Error};
use crate::{extract, session};
use std::fs;
use std::path::{Path, PathBuf};
fn container_with(at: &Path, name: &str, content_bytes: &[u8], extra: &str) -> PathBuf {
let doc: slpc::toml_edit::DocumentMut =
format!("slipcase_version = \"1.1\"\n{extra}\n[content]\nfile = \"{name}\"\n")
.parse()
.unwrap();
let path = at.join(format!("{name}.slpc"));
slpc::pack_reader(name, content_bytes, doc, fs::File::create(&path).unwrap()).unwrap();
path
}
fn opened(root: &Path, container: &Path, name: &str) -> session::Session {
let mut s = session::create(root, container, name).unwrap();
extract::extract(&mut slpc::Container::open(container).unwrap(), &mut s).unwrap();
s
}
fn content_of(container: &Path) -> Vec<u8> {
let mut c = slpc::Container::open(container).unwrap();
let mut out = Vec::new();
std::io::copy(&mut c.content().unwrap(), &mut out).unwrap();
out
}
#[test]
fn an_edit_reaches_the_container() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
write_back(&mut s).unwrap();
assert_eq!(content_of(&c), b"edited");
}
#[test]
fn the_flyleaf_member_is_returned_byte_for_byte() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let extra = "producer = \"something else\"\nsha256 = \"stale after this edit\"\n";
let c = container_with(tmp.path(), "report.pdf", b"first", extra);
let before = slpc::Container::open(&c).unwrap().flyleaf_bytes().to_vec();
let mut s = opened(&root, &c, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
write_back(&mut s).unwrap();
let after = slpc::Container::open(&c).unwrap().flyleaf_bytes().to_vec();
assert_eq!(before, after);
}
#[test]
fn the_content_file_keeps_the_name_the_session_recorded() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
write_back(&mut s).unwrap();
assert_eq!(
slpc::Container::open(&c).unwrap().content_name(),
"report.pdf"
);
}
#[test]
fn each_write_back_is_counted_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
for n in 1..=3 {
fs::write(s.content_path(), format!("edit {n}")).unwrap();
write_back(&mut s).unwrap();
assert_eq!(session::scan(&root).unwrap()[0].record().write_backs, n);
}
assert_eq!(content_of(&c), b"edit 3");
}
#[test]
fn writing_back_repeatedly_leaves_one_container_and_no_debris() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
for n in 0..5 {
fs::write(s.content_path(), format!("{n}")).unwrap();
write_back(&mut s).unwrap();
}
let beside: Vec<_> = fs::read_dir(tmp.path())
.unwrap()
.map(|e| e.unwrap().file_name())
.filter(|n| n != "sessions")
.collect();
assert_eq!(beside, ["report.pdf.slpc"]);
}
#[test]
fn a_container_that_went_away_is_reported_rather_than_recreated() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
fs::remove_file(&c).unwrap();
assert!(matches!(write_back(&mut s), Err(Error::Container(_))));
assert!(!c.exists());
}
#[test]
fn a_missing_content_file_is_reported_and_the_container_is_left_alone() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
let mut s = opened(&root, &c, "report.pdf");
fs::remove_file(s.content_path()).unwrap();
assert!(matches!(write_back(&mut s), Err(Error::Content(_))));
assert_eq!(content_of(&c), b"first");
}
#[test]
fn a_container_reached_through_a_link_replaces_the_file_and_not_the_link() {
#[cfg(unix)]
{
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let real = container_with(tmp.path(), "report.pdf", b"first", "");
let link = tmp.path().join("link.slpc");
std::os::unix::fs::symlink(&real, &link).unwrap();
let mut s = opened(&root, &link, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
write_back(&mut s).unwrap();
assert_eq!(content_of(&real), b"edited");
}
}
#[test]
fn a_marked_container_is_still_marked_after_a_write_back() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container_with(tmp.path(), "report.pdf", b"first", "");
assert!(
testsupport::mark_as_downloaded(&c),
"this filesystem would not hold the mark, so the carry is untested here"
);
let mut s = opened(&root, &c, "report.pdf");
fs::write(s.content_path(), b"edited").unwrap();
write_back(&mut s).unwrap();
assert!(slpc::provenance::arrived_from_elsewhere(&c));
assert_eq!(content_of(&c), b"edited");
}
}