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