1use std::fmt;
40use std::fs::File;
41
42use slpc::Destination;
43
44use crate::session::Session;
45
46#[derive(Debug)]
48pub enum Error {
49 Content(std::io::Error),
51 Container(std::io::Error),
55 ContainerChanged {
63 recorded: String,
65 found: String,
67 },
68 Repack(slpc::Error),
70 WouldNotBeConformant(String),
73 Swap(slpc::Error),
78}
79
80impl fmt::Display for Error {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 Self::Content(e) => write!(f, "the edited content file could not be read: {e}"),
84 Self::Container(e) => write!(f, "the container could not be opened: {e}"),
85 Self::ContainerChanged { recorded, found } => write!(
86 f,
87 "the container now holds {found} rather than {recorded}, so this is not the \
88 container this session was opened against. Nothing was changed."
89 ),
90 Self::Repack(e) => write!(f, "the container could not be rebuilt: {e}"),
91 Self::WouldNotBeConformant(v) => write!(
92 f,
93 "the container this would have written is {v}. Nothing was changed."
94 ),
95 Self::Swap(e) => write!(
96 f,
97 "the rebuilt container could not replace the original: {e}"
98 ),
99 }
100 }
101}
102
103impl std::error::Error for Error {}
104
105pub fn write_back(session: &mut Session) -> Result<(), Error> {
116 let container = session.record().container.clone();
117 let content_path = session.content_path();
118
119 let edited = File::open(&content_path).map_err(Error::Content)?;
120
121 let found = slpc::Container::open(&container)
126 .map_err(|e| match e {
127 slpc::Error::Io(e) => Error::Container(e),
128 other => Error::Repack(other),
129 })?
130 .content_name()
131 .to_string();
132 if found != session.record().content_name {
133 return Err(Error::ContainerChanged {
134 recorded: session.record().content_name.clone(),
135 found,
136 });
137 }
138
139 let source = File::open(&container).map_err(Error::Container)?;
140
141 let mut out = Destination::in_place(&container).map_err(Error::Swap)?;
146
147 slpc::Repack::new(source)
152 .content(&session.record().content_name, edited)
153 .write(out.writer())
154 .map_err(Error::Repack)?;
155
156 let verdict = slpc::validate(out.written().map_err(Error::Repack)?).map_err(Error::Repack)?;
157 if !verdict.is_conformant() {
158 return Err(Error::WouldNotBeConformant(verdict.to_string()));
159 }
160 out.commit().map_err(Error::Swap)?;
161
162 if let Ok(repacked) = slpc::Container::open(&container) {
172 if let Ok(crc) = repacked.content_crc() {
173 let _ = session.note_agreement(crc);
174 }
175 }
176
177 session.note_write_back().map_err(Error::Content)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{write_back, Error};
183 use crate::{extract, session};
184 use std::fs;
185 use std::path::{Path, PathBuf};
186
187 fn container_with(at: &Path, name: &str, content_bytes: &[u8], extra: &str) -> PathBuf {
188 let doc: slpc::toml_edit::DocumentMut =
189 format!("slipcase_version = \"1.1\"\n{extra}\n[content]\nfile = \"{name}\"\n")
190 .parse()
191 .unwrap();
192 let path = at.join(format!("{name}.slpc"));
193 slpc::pack_reader(name, content_bytes, doc, fs::File::create(&path).unwrap()).unwrap();
194 path
195 }
196
197 fn opened(root: &Path, container: &Path, name: &str) -> session::Session {
199 let mut s = session::create(root, container, name).unwrap();
200 extract::extract(&mut slpc::Container::open(container).unwrap(), &mut s).unwrap();
201 s
202 }
203
204 fn content_of(container: &Path) -> Vec<u8> {
205 let mut c = slpc::Container::open(container).unwrap();
206 let mut out = Vec::new();
207 std::io::copy(&mut c.content().unwrap(), &mut out).unwrap();
208 out
209 }
210
211 #[test]
212 fn an_edit_reaches_the_container() {
213 let tmp = tempfile::tempdir().unwrap();
214 let root = tmp.path().join("sessions");
215 let c = container_with(tmp.path(), "report.pdf", b"first", "");
216
217 let mut s = opened(&root, &c, "report.pdf");
218 fs::write(s.content_path(), b"edited").unwrap();
219 write_back(&mut s).unwrap();
220
221 assert_eq!(content_of(&c), b"edited");
222 }
223
224 #[test]
225 fn the_flyleaf_member_is_returned_byte_for_byte() {
226 let tmp = tempfile::tempdir().unwrap();
231 let root = tmp.path().join("sessions");
232 let extra = "producer = \"something else\"\nsha256 = \"stale after this edit\"\n";
233 let c = container_with(tmp.path(), "report.pdf", b"first", extra);
234
235 let before = slpc::Container::open(&c).unwrap().flyleaf_bytes().to_vec();
236 let mut s = opened(&root, &c, "report.pdf");
237 fs::write(s.content_path(), b"edited").unwrap();
238 write_back(&mut s).unwrap();
239
240 let after = slpc::Container::open(&c).unwrap().flyleaf_bytes().to_vec();
241 assert_eq!(before, after);
242 }
243
244 #[test]
245 fn the_content_file_keeps_the_name_the_session_recorded() {
246 let tmp = tempfile::tempdir().unwrap();
247 let root = tmp.path().join("sessions");
248 let c = container_with(tmp.path(), "report.pdf", b"first", "");
249
250 let mut s = opened(&root, &c, "report.pdf");
251 fs::write(s.content_path(), b"edited").unwrap();
252 write_back(&mut s).unwrap();
253
254 assert_eq!(
255 slpc::Container::open(&c).unwrap().content_name(),
256 "report.pdf"
257 );
258 }
259
260 #[test]
261 fn each_write_back_is_counted_on_disk() {
262 let tmp = tempfile::tempdir().unwrap();
263 let root = tmp.path().join("sessions");
264 let c = container_with(tmp.path(), "report.pdf", b"first", "");
265
266 let mut s = opened(&root, &c, "report.pdf");
267 for n in 1..=3 {
268 fs::write(s.content_path(), format!("edit {n}")).unwrap();
269 write_back(&mut s).unwrap();
270 assert_eq!(session::scan(&root).unwrap()[0].record().write_backs, n);
271 }
272 assert_eq!(content_of(&c), b"edit 3");
273 }
274
275 #[test]
276 fn writing_back_repeatedly_leaves_one_container_and_no_debris() {
277 let tmp = tempfile::tempdir().unwrap();
281 let root = tmp.path().join("sessions");
282 let c = container_with(tmp.path(), "report.pdf", b"first", "");
283
284 let mut s = opened(&root, &c, "report.pdf");
285 for n in 0..5 {
286 fs::write(s.content_path(), format!("{n}")).unwrap();
287 write_back(&mut s).unwrap();
288 }
289
290 let beside: Vec<_> = fs::read_dir(tmp.path())
291 .unwrap()
292 .map(|e| e.unwrap().file_name())
293 .filter(|n| n != "sessions")
294 .collect();
295 assert_eq!(beside, ["report.pdf.slpc"]);
296 }
297
298 #[test]
299 fn a_container_that_went_away_is_reported_rather_than_recreated() {
300 let tmp = tempfile::tempdir().unwrap();
304 let root = tmp.path().join("sessions");
305 let c = container_with(tmp.path(), "report.pdf", b"first", "");
306
307 let mut s = opened(&root, &c, "report.pdf");
308 fs::write(s.content_path(), b"edited").unwrap();
309 fs::remove_file(&c).unwrap();
310
311 assert!(matches!(write_back(&mut s), Err(Error::Container(_))));
312 assert!(!c.exists());
313 }
314
315 #[test]
316 fn a_missing_content_file_is_reported_and_the_container_is_left_alone() {
317 let tmp = tempfile::tempdir().unwrap();
318 let root = tmp.path().join("sessions");
319 let c = container_with(tmp.path(), "report.pdf", b"first", "");
320
321 let mut s = opened(&root, &c, "report.pdf");
322 fs::remove_file(s.content_path()).unwrap();
323
324 assert!(matches!(write_back(&mut s), Err(Error::Content(_))));
325 assert_eq!(content_of(&c), b"first");
326 }
327
328 #[test]
329 fn a_container_reached_through_a_link_replaces_the_file_and_not_the_link() {
330 #[cfg(unix)]
333 {
334 let tmp = tempfile::tempdir().unwrap();
335 let root = tmp.path().join("sessions");
336 let real = container_with(tmp.path(), "report.pdf", b"first", "");
337 let link = tmp.path().join("link.slpc");
338 std::os::unix::fs::symlink(&real, &link).unwrap();
339
340 let mut s = opened(&root, &link, "report.pdf");
341 fs::write(s.content_path(), b"edited").unwrap();
342 write_back(&mut s).unwrap();
343
344 assert_eq!(content_of(&real), b"edited");
345 }
346 }
347
348 #[test]
349 fn a_marked_container_is_still_marked_after_a_write_back() {
350 let tmp = tempfile::tempdir().unwrap();
356 let root = tmp.path().join("sessions");
357 let c = container_with(tmp.path(), "report.pdf", b"first", "");
358 assert!(
359 testsupport::mark_as_downloaded(&c),
360 "this filesystem would not hold the mark, so the carry is untested here"
361 );
362
363 let mut s = opened(&root, &c, "report.pdf");
364 fs::write(s.content_path(), b"edited").unwrap();
365 write_back(&mut s).unwrap();
366
367 assert!(slpc::provenance::arrived_from_elsewhere(&c));
368 assert_eq!(content_of(&c), b"edited");
369 }
370}