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 directory;
34mod localized;
35mod option;
36
37pub use blob::{blob_id, blob_id_of, BlobSource, Blobs};
38pub use directory::{directory_from_back, Back, Directory, Entry, EntryDecoder};
39pub use localized::{is_language_tag, Localized};
40pub use option::{Choice, Label, OptionDef, Options};
41
42pub const BUNDLE_FORMAT: u32 = 1;
45
46const HEAD_ENTRY: &str = "opys.json";
47const MANIFEST_ENTRY: &str = "manifest.json";
48
49fn blob_entry(id: &str) -> String {
50 format!("blobs/{id}")
51}
52
53#[derive(Debug, thiserror::Error)]
54pub enum BundleError {
55 #[error("not a bundle: {0}")]
56 Zip(#[from] zip::result::ZipError),
57 #[error(transparent)]
58 Io(#[from] io::Error),
59 #[error("not a bundle: it has no `{0}`")]
60 MissingEntry(&'static str),
61 #[error("`{entry}` does not parse: {source}")]
62 Json {
63 entry: &'static str,
64 #[source]
65 source: serde_json::Error,
66 },
67 #[error(
68 "bundle format {found} is not one this reader knows (it reads format {BUNDLE_FORMAT})"
69 )]
70 Format { found: u64 },
71 #[error("the manifest names blob {0}, and nothing holds it")]
72 MissingBlob(String),
73 #[error("blob {id} does not hold what its name says: its bytes hash to {found}")]
74 BlobMismatch { id: String, found: String },
75 #[error("cannot read blob {id} from {path}: {source}")]
76 BlobFile {
77 id: String,
78 path: String,
79 #[source]
80 source: io::Error,
81 },
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(from = "HeadWire", into = "HeadWire")]
90pub struct Head {
91 pub format: u32,
92 pub options: Options,
95}
96
97impl Head {
98 pub fn new(options: Options) -> Self {
100 Head {
101 format: BUNDLE_FORMAT,
102 options,
103 }
104 }
105}
106
107impl Default for Head {
108 fn default() -> Self {
109 Head::new(Options::default())
110 }
111}
112
113#[derive(Serialize, Deserialize)]
117pub(crate) struct HeadWire {
118 format: u32,
119 #[serde(default, skip_serializing_if = "Options::is_empty")]
120 options: Options,
121}
122
123impl From<HeadWire> for Head {
124 fn from(raw: HeadWire) -> Self {
125 Head {
126 format: raw.format,
127 options: raw.options,
128 }
129 }
130}
131
132impl From<Head> for HeadWire {
133 fn from(head: Head) -> Self {
134 HeadWire {
135 format: head.format,
136 options: head.options,
137 }
138 }
139}
140
141#[derive(Deserialize)]
150struct FormatProbe {
151 format: u64,
152}
153
154fn parse_head(bytes: &[u8]) -> Result<Head, BundleError> {
155 let json = |source| BundleError::Json {
156 entry: HEAD_ENTRY,
157 source,
158 };
159 let probe: FormatProbe = serde_json::from_slice(bytes).map_err(json)?;
160 if probe.format != u64::from(BUNDLE_FORMAT) {
161 return Err(BundleError::Format {
162 found: probe.format,
163 });
164 }
165 serde_json::from_slice(bytes).map_err(json)
166}
167
168fn parse_manifest(bytes: &[u8]) -> Result<Manifest, BundleError> {
169 serde_json::from_slice(bytes).map_err(|source| BundleError::Json {
170 entry: MANIFEST_ENTRY,
171 source,
172 })
173}
174
175fn read_entry<R: Read + Seek>(
176 archive: &mut ZipArchive<R>,
177 name: &'static str,
178) -> Result<Vec<u8>, BundleError> {
179 let mut entry = match archive.by_name(name) {
180 Ok(entry) => entry,
181 Err(zip::result::ZipError::FileNotFound) => return Err(BundleError::MissingEntry(name)),
182 Err(other) => return Err(other.into()),
183 };
184 let mut bytes = Vec::with_capacity(entry.size() as usize);
185 entry.read_to_end(&mut bytes)?;
186 Ok(bytes)
187}
188
189pub fn read_bundle_head<R: Read + Seek>(reader: R) -> Result<Head, BundleError> {
192 let mut archive = ZipArchive::new(reader)?;
193 parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)
194}
195
196#[derive(Debug, Clone, PartialEq)]
198pub enum Front {
199 Head(Head),
200 Needs(u64),
203}
204
205const LOCAL_HEADER: [u8; 4] = *b"PK\x03\x04";
206const LOCAL_HEADER_LEN: usize = 30;
207const SIZES_AFTER_DATA: u16 = 1 << 3;
209
210fn not_a_bundle(why: &'static str) -> BundleError {
211 BundleError::Zip(zip::result::ZipError::InvalidArchive(why))
212}
213
214pub fn head_from_front(front: &[u8]) -> Result<Front, BundleError> {
223 let Some(header) = front.get(..LOCAL_HEADER_LEN) else {
224 return Ok(Front::Needs(LOCAL_HEADER_LEN as u64));
225 };
226 if header[..4] != LOCAL_HEADER {
227 return Err(not_a_bundle("it does not begin with a zip entry"));
228 }
229 let short = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
230 let long = |at: usize| {
231 u32::from_le_bytes([header[at], header[at + 1], header[at + 2], header[at + 3]])
232 };
233 let (flags, method, size) = (short(6), short(8), u64::from(long(22)));
234 let (name_len, extra_len) = (usize::from(short(26)), usize::from(short(28)));
235
236 let name_end = LOCAL_HEADER_LEN + name_len;
237 let Some(name) = front.get(LOCAL_HEADER_LEN..name_end) else {
238 return Ok(Front::Needs(name_end as u64));
239 };
240 if name != HEAD_ENTRY.as_bytes() {
241 return Err(BundleError::MissingEntry(HEAD_ENTRY));
242 }
243 if method != 0 || flags & SIZES_AFTER_DATA != 0 {
244 return Err(not_a_bundle("its head is not stored"));
245 }
246 let start = name_end + extra_len;
247 let end = start as u64 + size;
248 match usize::try_from(end)
249 .ok()
250 .and_then(|end| front.get(start..end))
251 {
252 Some(head) => parse_head(head).map(Front::Head),
253 None => Ok(Front::Needs(end)),
254 }
255}
256
257pub struct Bundle<R> {
259 manifest: Manifest,
260 archive: ZipArchive<R>,
261}
262
263pub fn open_bundle<R: Read + Seek>(reader: R) -> Result<Bundle<R>, BundleError> {
266 let mut archive = ZipArchive::new(reader)?;
267 parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)?;
270 let manifest = parse_manifest(&read_entry(&mut archive, MANIFEST_ENTRY)?)?;
271 for id in manifest.blob_ids() {
272 if archive.index_for_name(&blob_entry(id)).is_none() {
273 return Err(BundleError::MissingBlob(id.to_owned()));
274 }
275 }
276 Ok(Bundle { manifest, archive })
277}
278
279impl<R: Read + Seek> Bundle<R> {
280 pub fn manifest(&self) -> &Manifest {
281 &self.manifest
282 }
283
284 pub fn into_manifest(self) -> Manifest {
285 self.manifest
286 }
287
288 pub fn copy_blob(&mut self, id: &str, out: &mut impl Write) -> Result<u64, BundleError> {
290 let mut entry = match self.archive.by_name(&blob_entry(id)) {
291 Ok(entry) => entry,
292 Err(zip::result::ZipError::FileNotFound) => {
293 return Err(BundleError::MissingBlob(id.to_owned()))
294 }
295 Err(other) => return Err(other.into()),
296 };
297 Ok(io::copy(&mut entry, out)?)
298 }
299}
300
301struct Hashing<W> {
304 inner: W,
305 hasher: Sha256,
306}
307
308impl<W: Write> Write for Hashing<W> {
309 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
310 let written = self.inner.write(buf)?;
311 self.hasher.update(&buf[..written]);
312 Ok(written)
313 }
314
315 fn flush(&mut self) -> io::Result<()> {
316 self.inner.flush()
317 }
318}
319
320fn entry_options(method: CompressionMethod, size: u64) -> SimpleFileOptions {
323 SimpleFileOptions::default()
324 .compression_method(method)
325 .last_modified_time(DateTime::default())
326 .unix_permissions(0o644)
327 .large_file(size > u64::from(u32::MAX))
328}
329
330fn write_json<W: Write + Seek>(
331 zip: &mut ZipWriter<W>,
332 entry: &'static str,
333 method: CompressionMethod,
334 bytes: &[u8],
335) -> Result<(), BundleError> {
336 zip.start_file(entry, entry_options(method, bytes.len() as u64))?;
337 zip.write_all(bytes)?;
338 Ok(())
339}
340
341pub fn write_bundle<W: Write + Seek>(
350 writer: W,
351 head: &Head,
352 manifest: &Manifest,
353 blobs: &Blobs,
354) -> Result<(), BundleError> {
355 if head.format != BUNDLE_FORMAT {
356 return Err(BundleError::Format {
357 found: u64::from(head.format),
358 });
359 }
360 let json = |entry| move |source| BundleError::Json { entry, source };
361 let mut zip = ZipWriter::new(writer);
362
363 let head = serde_json::to_vec_pretty(head).map_err(json(HEAD_ENTRY))?;
367 write_json(&mut zip, HEAD_ENTRY, CompressionMethod::Stored, &head)?;
368 let body = serde_json::to_vec_pretty(manifest).map_err(json(MANIFEST_ENTRY))?;
369 write_json(&mut zip, MANIFEST_ENTRY, CompressionMethod::Deflated, &body)?;
370
371 for id in manifest.blob_ids() {
372 let source = blobs
373 .get(id)
374 .ok_or_else(|| BundleError::MissingBlob(id.to_owned()))?;
375 let size = match source {
376 BlobSource::Bytes(bytes) => bytes.len() as u64,
377 BlobSource::File(path) => std::fs::metadata(path)
378 .map_err(|source| BundleError::BlobFile {
379 id: id.to_owned(),
380 path: path.display().to_string(),
381 source,
382 })?
383 .len(),
384 };
385 zip.start_file(
386 blob_entry(id),
387 entry_options(CompressionMethod::Deflated, size),
388 )?;
389 let mut hashing = Hashing {
390 inner: &mut zip,
391 hasher: Sha256::new(),
392 };
393 match source {
394 BlobSource::Bytes(bytes) => hashing.write_all(bytes)?,
395 BlobSource::File(path) => {
396 let mut file = File::open(path).map_err(|source| BundleError::BlobFile {
397 id: id.to_owned(),
398 path: path.display().to_string(),
399 source,
400 })?;
401 io::copy(&mut file, &mut hashing)?;
402 }
403 }
404 let found = hex::encode(hashing.hasher.finalize());
405 if found != id {
406 return Err(BundleError::BlobMismatch {
407 id: id.to_owned(),
408 found,
409 });
410 }
411 }
412
413 zip.finish()?;
414 Ok(())
415}