1use std::fs::File;
23use std::io::{self, Read, Seek, Write};
24
25use serde::{Deserialize, Serialize};
26use sha2::{Digest, Sha256};
27use zip::write::SimpleFileOptions;
28use zip::{CompressionMethod, DateTime, ZipArchive, ZipWriter};
29
30use opys_core::Manifest;
31
32mod blob;
33mod option;
34
35pub use blob::{blob_id, blob_id_of, BlobSource, Blobs};
36pub use option::{Choice, Label, OptionDef, Options};
37
38pub const BUNDLE_FORMAT: u32 = 1;
41
42const HEAD_ENTRY: &str = "opys.json";
43const MANIFEST_ENTRY: &str = "manifest.json";
44
45fn blob_entry(id: &str) -> String {
46 format!("blobs/{id}")
47}
48
49#[derive(Debug, thiserror::Error)]
50pub enum BundleError {
51 #[error("not a bundle: {0}")]
52 Zip(#[from] zip::result::ZipError),
53 #[error(transparent)]
54 Io(#[from] io::Error),
55 #[error("not a bundle: it has no `{0}`")]
56 MissingEntry(&'static str),
57 #[error("`{entry}` does not parse: {source}")]
58 Json {
59 entry: &'static str,
60 #[source]
61 source: serde_json::Error,
62 },
63 #[error(
64 "bundle format {found} is not one this reader knows (it reads format {BUNDLE_FORMAT})"
65 )]
66 Format { found: u64 },
67 #[error("the manifest names blob {0}, and nothing holds it")]
68 MissingBlob(String),
69 #[error("blob {id} does not hold what its name says: its bytes hash to {found}")]
70 BlobMismatch { id: String, found: String },
71 #[error("cannot read blob {id} from {path}: {source}")]
72 BlobFile {
73 id: String,
74 path: String,
75 #[source]
76 source: io::Error,
77 },
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct Head {
86 pub format: u32,
87 #[serde(default, skip_serializing_if = "Options::is_empty")]
90 pub options: Options,
91}
92
93impl Head {
94 pub fn new(options: Options) -> Self {
96 Head {
97 format: BUNDLE_FORMAT,
98 options,
99 }
100 }
101}
102
103impl Default for Head {
104 fn default() -> Self {
105 Head::new(Options::default())
106 }
107}
108
109#[derive(Deserialize)]
118struct FormatProbe {
119 format: u64,
120}
121
122fn parse_head(bytes: &[u8]) -> Result<Head, BundleError> {
123 let json = |source| BundleError::Json {
124 entry: HEAD_ENTRY,
125 source,
126 };
127 let probe: FormatProbe = serde_json::from_slice(bytes).map_err(json)?;
128 if probe.format != u64::from(BUNDLE_FORMAT) {
129 return Err(BundleError::Format {
130 found: probe.format,
131 });
132 }
133 serde_json::from_slice(bytes).map_err(json)
134}
135
136fn parse_manifest(bytes: &[u8]) -> Result<Manifest, BundleError> {
137 serde_json::from_slice(bytes).map_err(|source| BundleError::Json {
138 entry: MANIFEST_ENTRY,
139 source,
140 })
141}
142
143fn read_entry<R: Read + Seek>(
144 archive: &mut ZipArchive<R>,
145 name: &'static str,
146) -> Result<Vec<u8>, BundleError> {
147 let mut entry = match archive.by_name(name) {
148 Ok(entry) => entry,
149 Err(zip::result::ZipError::FileNotFound) => return Err(BundleError::MissingEntry(name)),
150 Err(other) => return Err(other.into()),
151 };
152 let mut bytes = Vec::with_capacity(entry.size() as usize);
153 entry.read_to_end(&mut bytes)?;
154 Ok(bytes)
155}
156
157pub fn read_bundle_head<R: Read + Seek>(reader: R) -> Result<Head, BundleError> {
160 let mut archive = ZipArchive::new(reader)?;
161 parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)
162}
163
164#[derive(Debug, Clone, PartialEq)]
166pub enum Front {
167 Head(Head),
168 Needs(u64),
171}
172
173const LOCAL_HEADER: [u8; 4] = *b"PK\x03\x04";
174const LOCAL_HEADER_LEN: usize = 30;
175const SIZES_AFTER_DATA: u16 = 1 << 3;
177
178fn not_a_bundle(why: &'static str) -> BundleError {
179 BundleError::Zip(zip::result::ZipError::InvalidArchive(why))
180}
181
182pub fn head_from_front(front: &[u8]) -> Result<Front, BundleError> {
191 let Some(header) = front.get(..LOCAL_HEADER_LEN) else {
192 return Ok(Front::Needs(LOCAL_HEADER_LEN as u64));
193 };
194 if header[..4] != LOCAL_HEADER {
195 return Err(not_a_bundle("it does not begin with a zip entry"));
196 }
197 let short = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
198 let long = |at: usize| {
199 u32::from_le_bytes([header[at], header[at + 1], header[at + 2], header[at + 3]])
200 };
201 let (flags, method, size) = (short(6), short(8), u64::from(long(22)));
202 let (name_len, extra_len) = (usize::from(short(26)), usize::from(short(28)));
203
204 let name_end = LOCAL_HEADER_LEN + name_len;
205 let Some(name) = front.get(LOCAL_HEADER_LEN..name_end) else {
206 return Ok(Front::Needs(name_end as u64));
207 };
208 if name != HEAD_ENTRY.as_bytes() {
209 return Err(BundleError::MissingEntry(HEAD_ENTRY));
210 }
211 if method != 0 || flags & SIZES_AFTER_DATA != 0 {
212 return Err(not_a_bundle("its head is not stored"));
213 }
214 let start = name_end + extra_len;
215 let end = start as u64 + size;
216 match usize::try_from(end)
217 .ok()
218 .and_then(|end| front.get(start..end))
219 {
220 Some(head) => parse_head(head).map(Front::Head),
221 None => Ok(Front::Needs(end)),
222 }
223}
224
225pub struct Bundle<R> {
227 manifest: Manifest,
228 archive: ZipArchive<R>,
229}
230
231pub fn open_bundle<R: Read + Seek>(reader: R) -> Result<Bundle<R>, BundleError> {
234 let mut archive = ZipArchive::new(reader)?;
235 parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)?;
238 let manifest = parse_manifest(&read_entry(&mut archive, MANIFEST_ENTRY)?)?;
239 for id in manifest.blob_ids() {
240 if archive.index_for_name(&blob_entry(id)).is_none() {
241 return Err(BundleError::MissingBlob(id.to_owned()));
242 }
243 }
244 Ok(Bundle { manifest, archive })
245}
246
247impl<R: Read + Seek> Bundle<R> {
248 pub fn manifest(&self) -> &Manifest {
249 &self.manifest
250 }
251
252 pub fn into_manifest(self) -> Manifest {
253 self.manifest
254 }
255
256 pub fn copy_blob(&mut self, id: &str, out: &mut impl Write) -> Result<u64, BundleError> {
258 let mut entry = match self.archive.by_name(&blob_entry(id)) {
259 Ok(entry) => entry,
260 Err(zip::result::ZipError::FileNotFound) => {
261 return Err(BundleError::MissingBlob(id.to_owned()))
262 }
263 Err(other) => return Err(other.into()),
264 };
265 Ok(io::copy(&mut entry, out)?)
266 }
267}
268
269struct Hashing<W> {
272 inner: W,
273 hasher: Sha256,
274}
275
276impl<W: Write> Write for Hashing<W> {
277 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
278 let written = self.inner.write(buf)?;
279 self.hasher.update(&buf[..written]);
280 Ok(written)
281 }
282
283 fn flush(&mut self) -> io::Result<()> {
284 self.inner.flush()
285 }
286}
287
288fn entry_options(method: CompressionMethod, size: u64) -> SimpleFileOptions {
291 SimpleFileOptions::default()
292 .compression_method(method)
293 .last_modified_time(DateTime::default())
294 .unix_permissions(0o644)
295 .large_file(size > u64::from(u32::MAX))
296}
297
298fn write_json<W: Write + Seek>(
299 zip: &mut ZipWriter<W>,
300 entry: &'static str,
301 method: CompressionMethod,
302 bytes: &[u8],
303) -> Result<(), BundleError> {
304 zip.start_file(entry, entry_options(method, bytes.len() as u64))?;
305 zip.write_all(bytes)?;
306 Ok(())
307}
308
309pub fn write_bundle<W: Write + Seek>(
318 writer: W,
319 head: &Head,
320 manifest: &Manifest,
321 blobs: &Blobs,
322) -> Result<(), BundleError> {
323 if head.format != BUNDLE_FORMAT {
324 return Err(BundleError::Format {
325 found: u64::from(head.format),
326 });
327 }
328 let json = |entry| move |source| BundleError::Json { entry, source };
329 let mut zip = ZipWriter::new(writer);
330
331 let head = serde_json::to_vec_pretty(head).map_err(json(HEAD_ENTRY))?;
335 write_json(&mut zip, HEAD_ENTRY, CompressionMethod::Stored, &head)?;
336 let body = serde_json::to_vec_pretty(manifest).map_err(json(MANIFEST_ENTRY))?;
337 write_json(&mut zip, MANIFEST_ENTRY, CompressionMethod::Deflated, &body)?;
338
339 for id in manifest.blob_ids() {
340 let source = blobs
341 .get(id)
342 .ok_or_else(|| BundleError::MissingBlob(id.to_owned()))?;
343 let size = match source {
344 BlobSource::Bytes(bytes) => bytes.len() as u64,
345 BlobSource::File(path) => std::fs::metadata(path)
346 .map_err(|source| BundleError::BlobFile {
347 id: id.to_owned(),
348 path: path.display().to_string(),
349 source,
350 })?
351 .len(),
352 };
353 zip.start_file(
354 blob_entry(id),
355 entry_options(CompressionMethod::Deflated, size),
356 )?;
357 let mut hashing = Hashing {
358 inner: &mut zip,
359 hasher: Sha256::new(),
360 };
361 match source {
362 BlobSource::Bytes(bytes) => hashing.write_all(bytes)?,
363 BlobSource::File(path) => {
364 let mut file = File::open(path).map_err(|source| BundleError::BlobFile {
365 id: id.to_owned(),
366 path: path.display().to_string(),
367 source,
368 })?;
369 io::copy(&mut file, &mut hashing)?;
370 }
371 }
372 let found = hex::encode(hashing.hasher.finalize());
373 if found != id {
374 return Err(BundleError::BlobMismatch {
375 id: id.to_owned(),
376 found,
377 });
378 }
379 }
380
381 zip.finish()?;
382 Ok(())
383}