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
164pub struct Bundle<R> {
166 manifest: Manifest,
167 archive: ZipArchive<R>,
168}
169
170pub fn open_bundle<R: Read + Seek>(reader: R) -> Result<Bundle<R>, BundleError> {
173 let mut archive = ZipArchive::new(reader)?;
174 parse_head(&read_entry(&mut archive, HEAD_ENTRY)?)?;
177 let manifest = parse_manifest(&read_entry(&mut archive, MANIFEST_ENTRY)?)?;
178 for id in manifest.blob_ids() {
179 if archive.index_for_name(&blob_entry(id)).is_none() {
180 return Err(BundleError::MissingBlob(id.to_owned()));
181 }
182 }
183 Ok(Bundle { manifest, archive })
184}
185
186impl<R: Read + Seek> Bundle<R> {
187 pub fn manifest(&self) -> &Manifest {
188 &self.manifest
189 }
190
191 pub fn into_manifest(self) -> Manifest {
192 self.manifest
193 }
194
195 pub fn copy_blob(&mut self, id: &str, out: &mut impl Write) -> Result<u64, BundleError> {
197 let mut entry = match self.archive.by_name(&blob_entry(id)) {
198 Ok(entry) => entry,
199 Err(zip::result::ZipError::FileNotFound) => {
200 return Err(BundleError::MissingBlob(id.to_owned()))
201 }
202 Err(other) => return Err(other.into()),
203 };
204 Ok(io::copy(&mut entry, out)?)
205 }
206}
207
208struct Hashing<W> {
211 inner: W,
212 hasher: Sha256,
213}
214
215impl<W: Write> Write for Hashing<W> {
216 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
217 let written = self.inner.write(buf)?;
218 self.hasher.update(&buf[..written]);
219 Ok(written)
220 }
221
222 fn flush(&mut self) -> io::Result<()> {
223 self.inner.flush()
224 }
225}
226
227fn entry_options(method: CompressionMethod, size: u64) -> SimpleFileOptions {
230 SimpleFileOptions::default()
231 .compression_method(method)
232 .last_modified_time(DateTime::default())
233 .unix_permissions(0o644)
234 .large_file(size > u64::from(u32::MAX))
235}
236
237fn write_json<W: Write + Seek>(
238 zip: &mut ZipWriter<W>,
239 entry: &'static str,
240 method: CompressionMethod,
241 bytes: &[u8],
242) -> Result<(), BundleError> {
243 zip.start_file(entry, entry_options(method, bytes.len() as u64))?;
244 zip.write_all(bytes)?;
245 Ok(())
246}
247
248pub fn write_bundle<W: Write + Seek>(
257 writer: W,
258 head: &Head,
259 manifest: &Manifest,
260 blobs: &Blobs,
261) -> Result<(), BundleError> {
262 if head.format != BUNDLE_FORMAT {
263 return Err(BundleError::Format {
264 found: u64::from(head.format),
265 });
266 }
267 let json = |entry| move |source| BundleError::Json { entry, source };
268 let mut zip = ZipWriter::new(writer);
269
270 let head = serde_json::to_vec_pretty(head).map_err(json(HEAD_ENTRY))?;
271 write_json(&mut zip, HEAD_ENTRY, CompressionMethod::Stored, &head)?;
272 let body = serde_json::to_vec(manifest).map_err(json(MANIFEST_ENTRY))?;
273 write_json(&mut zip, MANIFEST_ENTRY, CompressionMethod::Deflated, &body)?;
274
275 for id in manifest.blob_ids() {
276 let source = blobs
277 .get(id)
278 .ok_or_else(|| BundleError::MissingBlob(id.to_owned()))?;
279 let size = match source {
280 BlobSource::Bytes(bytes) => bytes.len() as u64,
281 BlobSource::File(path) => std::fs::metadata(path)
282 .map_err(|source| BundleError::BlobFile {
283 id: id.to_owned(),
284 path: path.display().to_string(),
285 source,
286 })?
287 .len(),
288 };
289 zip.start_file(
290 blob_entry(id),
291 entry_options(CompressionMethod::Deflated, size),
292 )?;
293 let mut hashing = Hashing {
294 inner: &mut zip,
295 hasher: Sha256::new(),
296 };
297 match source {
298 BlobSource::Bytes(bytes) => hashing.write_all(bytes)?,
299 BlobSource::File(path) => {
300 let mut file = File::open(path).map_err(|source| BundleError::BlobFile {
301 id: id.to_owned(),
302 path: path.display().to_string(),
303 source,
304 })?;
305 io::copy(&mut file, &mut hashing)?;
306 }
307 }
308 let found = hex::encode(hashing.hasher.finalize());
309 if found != id {
310 return Err(BundleError::BlobMismatch {
311 id: id.to_owned(),
312 found,
313 });
314 }
315 }
316
317 zip.finish()?;
318 Ok(())
319}