use std::fs::File;
use std::io::{self, Read, Seek, Write};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, DateTime, ZipArchive, ZipWriter};
use opys_core::Manifest;
mod blob;
mod option;
pub use blob::{blob_id, blob_id_of, BlobSource, Blobs};
pub use option::{Choice, Label, OptionDef, Options};
pub const BUNDLE_FORMAT: u32 = 1;
const HEAD_ENTRY: &str = "opys.json";
const MANIFEST_ENTRY: &str = "manifest.json";
fn blob_entry(id: &str) -> String {
format!("blobs/{id}")
}
#[derive(Debug, thiserror::Error)]
pub enum BundleError {
#[error("not a bundle: {0}")]
Zip(#[from] zip::result::ZipError),
#[error(transparent)]
Io(#[from] io::Error),
#[error("not a bundle: it has no `{0}`")]
MissingEntry(&'static str),
#[error("`{entry}` does not parse: {source}")]
Json {
entry: &'static str,
#[source]
source: serde_json::Error,
},
#[error(
"bundle format {found} is not one this reader knows (it reads format {BUNDLE_FORMAT})"
)]
Format { found: u64 },
#[error("the manifest names blob {0}, and nothing holds it")]
MissingBlob(String),
#[error("blob {id} does not hold what its name says: its bytes hash to {found}")]
BlobMismatch { id: String, found: String },
#[error("cannot read blob {id} from {path}: {source}")]
BlobFile {
id: String,
path: String,
#[source]
source: io::Error,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Head {
pub format: u32,
#[serde(default, skip_serializing_if = "Options::is_empty")]
pub options: Options,
}
impl Head {
pub fn new(options: Options) -> Self {
Head {
format: BUNDLE_FORMAT,
options,
}
}
}
impl Default for Head {
fn default() -> Self {
Head::new(Options::default())
}
}
#[derive(Deserialize)]
struct FormatProbe {
format: u64,
}
fn parse_head(bytes: &[u8]) -> Result<Head, BundleError> {
let json = |source| BundleError::Json {
entry: HEAD_ENTRY,
source,
};
let probe: FormatProbe = serde_json::from_slice(bytes).map_err(json)?;
if probe.format != u64::from(BUNDLE_FORMAT) {
return Err(BundleError::Format {
found: probe.format,
});
}
serde_json::from_slice(bytes).map_err(json)
}
fn parse_manifest(bytes: &[u8]) -> Result<Manifest, BundleError> {
serde_json::from_slice(bytes).map_err(|source| BundleError::Json {
entry: MANIFEST_ENTRY,
source,
})
}
fn read_entry<R: Read + Seek>(
archive: &mut ZipArchive<R>,
name: &'static str,
) -> Result<Vec<u8>, BundleError> {
let mut entry = match archive.by_name(name) {
Ok(entry) => entry,
Err(zip::result::ZipError::FileNotFound) => return Err(BundleError::MissingEntry(name)),
Err(other) => return Err(other.into()),
};
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut bytes)?;
Ok(bytes)
}
pub fn read_bundle_head<R: Read + Seek>(reader: R) -> Result<Head, BundleError> {
let mut archive = ZipArchive::new(reader)?;
parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)
}
#[derive(Debug, Clone, PartialEq)]
pub enum Front {
Head(Head),
Needs(u64),
}
const LOCAL_HEADER: [u8; 4] = *b"PK\x03\x04";
const LOCAL_HEADER_LEN: usize = 30;
const SIZES_AFTER_DATA: u16 = 1 << 3;
fn not_a_bundle(why: &'static str) -> BundleError {
BundleError::Zip(zip::result::ZipError::InvalidArchive(why))
}
pub fn head_from_front(front: &[u8]) -> Result<Front, BundleError> {
let Some(header) = front.get(..LOCAL_HEADER_LEN) else {
return Ok(Front::Needs(LOCAL_HEADER_LEN as u64));
};
if header[..4] != LOCAL_HEADER {
return Err(not_a_bundle("it does not begin with a zip entry"));
}
let short = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
let long = |at: usize| {
u32::from_le_bytes([header[at], header[at + 1], header[at + 2], header[at + 3]])
};
let (flags, method, size) = (short(6), short(8), u64::from(long(22)));
let (name_len, extra_len) = (usize::from(short(26)), usize::from(short(28)));
let name_end = LOCAL_HEADER_LEN + name_len;
let Some(name) = front.get(LOCAL_HEADER_LEN..name_end) else {
return Ok(Front::Needs(name_end as u64));
};
if name != HEAD_ENTRY.as_bytes() {
return Err(BundleError::MissingEntry(HEAD_ENTRY));
}
if method != 0 || flags & SIZES_AFTER_DATA != 0 {
return Err(not_a_bundle("its head is not stored"));
}
let start = name_end + extra_len;
let end = start as u64 + size;
match usize::try_from(end)
.ok()
.and_then(|end| front.get(start..end))
{
Some(head) => parse_head(head).map(Front::Head),
None => Ok(Front::Needs(end)),
}
}
pub struct Bundle<R> {
manifest: Manifest,
archive: ZipArchive<R>,
}
pub fn open_bundle<R: Read + Seek>(reader: R) -> Result<Bundle<R>, BundleError> {
let mut archive = ZipArchive::new(reader)?;
parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)?;
let manifest = parse_manifest(&read_entry(&mut archive, MANIFEST_ENTRY)?)?;
for id in manifest.blob_ids() {
if archive.index_for_name(&blob_entry(id)).is_none() {
return Err(BundleError::MissingBlob(id.to_owned()));
}
}
Ok(Bundle { manifest, archive })
}
impl<R: Read + Seek> Bundle<R> {
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
pub fn into_manifest(self) -> Manifest {
self.manifest
}
pub fn copy_blob(&mut self, id: &str, out: &mut impl Write) -> Result<u64, BundleError> {
let mut entry = match self.archive.by_name(&blob_entry(id)) {
Ok(entry) => entry,
Err(zip::result::ZipError::FileNotFound) => {
return Err(BundleError::MissingBlob(id.to_owned()))
}
Err(other) => return Err(other.into()),
};
Ok(io::copy(&mut entry, out)?)
}
}
struct Hashing<W> {
inner: W,
hasher: Sha256,
}
impl<W: Write> Write for Hashing<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let written = self.inner.write(buf)?;
self.hasher.update(&buf[..written]);
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
fn entry_options(method: CompressionMethod, size: u64) -> SimpleFileOptions {
SimpleFileOptions::default()
.compression_method(method)
.last_modified_time(DateTime::default())
.unix_permissions(0o644)
.large_file(size > u64::from(u32::MAX))
}
fn write_json<W: Write + Seek>(
zip: &mut ZipWriter<W>,
entry: &'static str,
method: CompressionMethod,
bytes: &[u8],
) -> Result<(), BundleError> {
zip.start_file(entry, entry_options(method, bytes.len() as u64))?;
zip.write_all(bytes)?;
Ok(())
}
pub fn write_bundle<W: Write + Seek>(
writer: W,
head: &Head,
manifest: &Manifest,
blobs: &Blobs,
) -> Result<(), BundleError> {
if head.format != BUNDLE_FORMAT {
return Err(BundleError::Format {
found: u64::from(head.format),
});
}
let json = |entry| move |source| BundleError::Json { entry, source };
let mut zip = ZipWriter::new(writer);
let head = serde_json::to_vec_pretty(head).map_err(json(HEAD_ENTRY))?;
write_json(&mut zip, HEAD_ENTRY, CompressionMethod::Stored, &head)?;
let body = serde_json::to_vec_pretty(manifest).map_err(json(MANIFEST_ENTRY))?;
write_json(&mut zip, MANIFEST_ENTRY, CompressionMethod::Deflated, &body)?;
for id in manifest.blob_ids() {
let source = blobs
.get(id)
.ok_or_else(|| BundleError::MissingBlob(id.to_owned()))?;
let size = match source {
BlobSource::Bytes(bytes) => bytes.len() as u64,
BlobSource::File(path) => std::fs::metadata(path)
.map_err(|source| BundleError::BlobFile {
id: id.to_owned(),
path: path.display().to_string(),
source,
})?
.len(),
};
zip.start_file(
blob_entry(id),
entry_options(CompressionMethod::Deflated, size),
)?;
let mut hashing = Hashing {
inner: &mut zip,
hasher: Sha256::new(),
};
match source {
BlobSource::Bytes(bytes) => hashing.write_all(bytes)?,
BlobSource::File(path) => {
let mut file = File::open(path).map_err(|source| BundleError::BlobFile {
id: id.to_owned(),
path: path.display().to_string(),
source,
})?;
io::copy(&mut file, &mut hashing)?;
}
}
let found = hex::encode(hashing.hasher.finalize());
if found != id {
return Err(BundleError::BlobMismatch {
id: id.to_owned(),
found,
});
}
}
zip.finish()?;
Ok(())
}