use std::fmt;
use std::io::{Read, Seek};
use std::path::Path;
use slpc::provenance::Mark;
use slpc::{Container, Destination};
use crate::session::Session;
#[derive(Debug)]
pub enum Error {
Unreadable(slpc::Error),
Write(slpc::Error),
Unmarked {
cause: slpc::Error,
payload_removed: bool,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unreadable(e) => write!(f, "the payload cannot be read: {e}"),
Self::Write(e) => write!(f, "the payload could not be written: {e}"),
Self::Unmarked {
cause,
payload_removed,
} => write!(
f,
"where the container came from could not be carried onto its payload: {cause}\n\
The payload {}, because opening it would not raise the warning the container \
would have.",
if *payload_removed {
"has been removed"
} else {
"could not be removed either and is ungated"
}
),
}
}
}
impl std::error::Error for Error {}
pub fn extract<R: Read + Seek>(
container: &mut Container<R>,
session: &mut Session,
) -> Result<Mark, Error> {
container
.check_payload_readable()
.map_err(|u| Error::Unreadable(u.into()))?;
let out = session.payload_path();
let mut dest = Destination::new(&out, false).map_err(Error::Write)?;
{
let mut payload = container.payload().map_err(Error::Unreadable)?;
std::io::copy(&mut payload, dest.writer()).map_err(|e| Error::Write(e.into()))?;
}
dest.commit().map_err(Error::Write)?;
if let Ok(crc) = container.payload_crc() {
let _ = session.note_agreement(crc);
}
match slpc::provenance::carry(&session.record().container, &out) {
Ok(mark) => Ok(mark),
Err(cause) => Err(Error::Unmarked {
payload_removed: remove(&out),
cause,
}),
}
}
fn remove(at: &Path) -> bool {
std::fs::remove_file(at).is_ok()
}
#[cfg(test)]
mod tests {
use super::{extract, Error};
use crate::session;
use std::fs;
use std::path::{Path, PathBuf};
fn container(at: &Path, name: &str, payload: &[u8]) -> PathBuf {
let doc: slpc::toml_edit::DocumentMut =
format!("slipcase_version = \"1.0\"\n\n[payload]\nfile = \"{name}\"\n")
.parse()
.unwrap();
let path = at.join(format!("{name}.slpc"));
let out = fs::File::create(&path).unwrap();
slpc::pack_reader(name, payload, doc, out).unwrap();
path
}
fn open(at: &Path) -> slpc::Container<fs::File> {
slpc::Container::open(at).unwrap()
}
#[test]
fn the_payload_lands_under_its_own_name_with_its_own_bytes() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "report.pdf", b"%PDF-1.7 not really\n");
let mut s = session::create(&root, &c, "report.pdf").unwrap();
extract(&mut open(&c), &mut s).unwrap();
assert_eq!(s.payload_path().file_name().unwrap(), "report.pdf");
assert_eq!(
fs::read(s.payload_path()).unwrap(),
b"%PDF-1.7 not really\n"
);
}
#[test]
fn a_zero_length_payload_is_written_rather_than_refused() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "empty.txt", b"");
let mut s = session::create(&root, &c, "empty.txt").unwrap();
extract(&mut open(&c), &mut s).unwrap();
assert_eq!(fs::read(s.payload_path()).unwrap(), b"");
}
#[test]
fn the_payload_is_the_only_thing_written_into_the_payload_directory() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "report.pdf", b"x");
let mut s = session::create(&root, &c, "report.pdf").unwrap();
extract(&mut open(&c), &mut s).unwrap();
let mut found: Vec<_> = fs::read_dir(s.payload_dir())
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
found.sort();
assert_eq!(found, ["report.pdf"]);
}
#[test]
fn extracting_twice_into_one_session_refuses_rather_than_replaces() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "report.pdf", b"first");
let mut s = session::create(&root, &c, "report.pdf").unwrap();
extract(&mut open(&c), &mut s).unwrap();
fs::write(s.payload_path(), b"edited by somebody").unwrap();
assert!(matches!(
extract(&mut open(&c), &mut s),
Err(Error::Write(_))
));
assert_eq!(fs::read(s.payload_path()).unwrap(), b"edited by somebody");
}
#[test]
fn a_container_that_arrived_from_elsewhere_marks_the_payload_it_yields() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "report.pdf", b"%PDF");
assert!(
testsupport::mark_as_downloaded(&c),
"this filesystem would not hold the mark, so the carry is untested here"
);
let mut s = session::create(&root, &c, "report.pdf").unwrap();
let mark = extract(&mut open(&c), &mut s).unwrap();
assert_ne!(mark, slpc::provenance::Mark::Silent);
assert!(slpc::provenance::arrived_from_elsewhere(&s.payload_path()));
}
#[test]
fn a_container_that_says_nothing_yields_a_payload_that_says_nothing() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = container(tmp.path(), "report.pdf", b"%PDF");
let mut s = session::create(&root, &c, "report.pdf").unwrap();
let mark = extract(&mut open(&c), &mut s).unwrap();
assert_eq!(mark, slpc::provenance::Mark::Silent);
}
#[test]
fn an_unreadable_payload_is_refused_before_anything_is_written() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("sessions");
let c = tmp.path().join("locked.slpc");
fs::write(&c, encrypted_container()).unwrap();
let mut s = session::create(&root, &c, "secret.pdf").unwrap();
assert!(matches!(
extract(&mut open(&c), &mut s),
Err(Error::Unreadable(_))
));
assert!(!s.payload_path().exists());
assert_eq!(fs::read_dir(s.payload_dir()).unwrap().count(), 0);
}
fn encrypted_container() -> Vec<u8> {
let doc = "slipcase_version = \"1.0\"\n\n[payload]\nfile = \"secret.pdf\"\n";
let mut bytes = Vec::new();
{
let mut w = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes));
let opts: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default();
w.start_file(slpc::METADATA_MEMBER, opts).unwrap();
std::io::Write::write_all(&mut w, doc.as_bytes()).unwrap();
w.start_file("secret.pdf", opts).unwrap();
std::io::Write::write_all(&mut w, b"ciphertext").unwrap();
w.finish().unwrap();
}
set_encrypted_flag(&mut bytes);
bytes
}
fn set_encrypted_flag(bytes: &mut [u8]) {
for (signature, flag, len_at, name_at) in [
([0x50u8, 0x4b, 0x03, 0x04], 6usize, 26usize, 30usize),
([0x50, 0x4b, 0x01, 0x02], 8, 28, 46),
] {
for i in 0..bytes.len().saturating_sub(name_at) {
if bytes[i..i + 4] != signature {
continue;
}
let n = u16::from_le_bytes([bytes[i + len_at], bytes[i + len_at + 1]]) as usize;
let from = i + name_at;
if bytes.get(from..from + n) == Some(b"secret.pdf".as_slice()) {
bytes[i + flag] |= 0x01;
}
}
}
}
}