Skip to main content

opys_bundle/
directory.rs

1//! A bundle read in pieces: where each entry is in the file, and an entry
2//! decoded from its own bytes alone.
3//!
4//! A bundle behind a URL is not downloaded to be installed. Its directory is
5//! at the end of the file and says where every entry is; the head and the
6//! manifest are the first two entries; a blob is fetched only when the
7//! install finds it missing. This is the zip half of that — byte slices in,
8//! entries out — and the requests are the runtime's.
9//!
10//! An entry is taken to run from its local header to the next entry's, or to
11//! the directory: a zip is its entries end to end, so that range holds the
12//! header, the data and whatever trails it, without trusting the local
13//! header's own lengths to find where the next begins.
14
15use std::collections::BTreeMap;
16use std::io::{self, Write};
17use std::ops::Range;
18
19use flate2::write::DeflateDecoder;
20use flate2::Crc;
21use opys_core::Manifest;
22
23use crate::{
24    blob_entry, not_a_bundle, parse_head, parse_manifest, BundleError, HEAD_ENTRY, LOCAL_HEADER,
25    LOCAL_HEADER_LEN, MANIFEST_ENTRY,
26};
27
28const END: [u8; 4] = *b"PK\x05\x06";
29const END_LEN: usize = 22;
30const END64_LOCATOR: [u8; 4] = *b"PK\x06\x07";
31const END64_LOCATOR_LEN: usize = 20;
32const END64: [u8; 4] = *b"PK\x06\x06";
33const END64_LEN: usize = 56;
34const CENTRAL: [u8; 4] = *b"PK\x01\x02";
35const CENTRAL_LEN: usize = 46;
36/// The longest a zip's comment can be, and so how far from the end its
37/// end-of-directory record can sit.
38const LONGEST_COMMENT: usize = u16::MAX as usize;
39const ZIP64_EXTRA: u16 = 0x0001;
40const ENCRYPTED: u16 = 1;
41
42const STORED: u16 = 0;
43const DEFLATED: u16 = 8;
44
45/// One entry: where its bytes are in the file, and what they decode to.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Entry {
48    range: Range<u64>,
49    method: u16,
50    compressed: u64,
51    size: u64,
52    crc: u32,
53}
54
55impl Entry {
56    /// The bytes of the file that hold the entry, header to trailer.
57    pub fn range(&self) -> Range<u64> {
58        self.range.clone()
59    }
60
61    /// What the entry decodes to, in bytes.
62    pub fn size(&self) -> u64 {
63        self.size
64    }
65
66    /// A writer that takes the bytes of [`Entry::range`], in order and in
67    /// pieces of any size, and writes what they decode to into `out`.
68    pub fn decoder<W: Write>(&self, out: W) -> EntryDecoder<W> {
69        EntryDecoder {
70            entry: self.clone(),
71            state: State::Header(Vec::new()),
72            out: Some(out),
73        }
74    }
75
76    /// The entry decoded from `raw`, the bytes of its range.
77    fn decode(&self, raw: &[u8]) -> Result<Vec<u8>, BundleError> {
78        let mut decoder = self.decoder(Vec::with_capacity(self.size as usize));
79        decoder.write_all(raw)?;
80        decoder.finish()
81    }
82}
83
84/// Where every entry of a bundle is, read off the end of the file.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Directory {
87    len: u64,
88    entries: BTreeMap<String, Entry>,
89}
90
91/// What the last bytes of a bundle give of its directory.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum Back {
94    Directory(Directory),
95    /// The directory begins before what was given: it takes this many bytes,
96    /// counted from the end of the file — always more than were given.
97    Needs(u64),
98}
99
100fn short(bytes: &[u8], at: usize) -> u16 {
101    u16::from_le_bytes([bytes[at], bytes[at + 1]])
102}
103
104fn long(bytes: &[u8], at: usize) -> u32 {
105    u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
106}
107
108fn wide(bytes: &[u8], at: usize) -> u64 {
109    let mut word = [0; 8];
110    word.copy_from_slice(&bytes[at..at + 8]);
111    u64::from_le_bytes(word)
112}
113
114/// The end-of-directory record's place in `tail`. It is the last thing in
115/// the file but a comment, so it is looked for from the back, and a match is
116/// only one whose comment runs exactly to the end.
117fn end_record(tail: &[u8]) -> Option<usize> {
118    let last = tail.len().checked_sub(END_LEN)?;
119    (0..=last).rev().find(|&at| {
120        tail[at..at + 4] == END && usize::from(short(tail, at + 20)) == tail.len() - at - END_LEN
121    })
122}
123
124/// Read the directory of a bundle `file_len` bytes long from `tail`, its
125/// last bytes. Asks for more, by [`Back::Needs`], until the tail holds it.
126pub fn directory_from_back(tail: &[u8], file_len: u64) -> Result<Back, BundleError> {
127    let given = tail.len() as u64;
128    if given > file_len {
129        return Err(not_a_bundle("it is shorter than its own tail"));
130    }
131    // Where an offset counted from the start of the file is in the tail.
132    let at = |offset: u64| offset.checked_sub(file_len - given).map(|at| at as usize);
133    let short_of = |offset: u64| Ok(Back::Needs(file_len - offset));
134
135    let Some(end) = end_record(tail) else {
136        let reach = file_len.min((END_LEN + LONGEST_COMMENT) as u64);
137        return if given < reach {
138            Ok(Back::Needs(reach))
139        } else {
140            Err(not_a_bundle("it has no zip directory"))
141        };
142    };
143    let (mut count, mut size, mut offset) = (
144        u64::from(short(tail, end + 10)),
145        u64::from(long(tail, end + 12)),
146        u64::from(long(tail, end + 16)),
147    );
148    // A zip too large for the classic record says so with all ones and keeps
149    // the real numbers in a zip64 record, found through a locator right
150    // before the classic one.
151    if count == u64::from(u16::MAX) || size == u64::from(u32::MAX) || offset == u64::from(u32::MAX)
152    {
153        let locator = end
154            .checked_sub(END64_LOCATOR_LEN)
155            .filter(|&locator| tail[locator..locator + 4] == END64_LOCATOR)
156            .ok_or_else(|| not_a_bundle("its zip64 directory has no locator"))?;
157        let record = wide(tail, locator + 8);
158        let Some(record) = at(record) else {
159            return short_of(record);
160        };
161        if tail
162            .get(record..record + END64_LEN)
163            .map_or(true, |r| r[..4] != END64)
164        {
165            return Err(not_a_bundle(
166                "its zip64 directory is not where its locator says",
167            ));
168        }
169        (count, size, offset) = (
170            wide(tail, record + 32),
171            wide(tail, record + 40),
172            wide(tail, record + 48),
173        );
174    }
175    let Some(start) = at(offset) else {
176        return short_of(offset);
177    };
178    let central = usize::try_from(size)
179        .ok()
180        .and_then(|size| tail.get(start..start.checked_add(size)?))
181        .ok_or_else(|| not_a_bundle("its directory runs past its end"))?;
182
183    let mut found: Vec<(String, Entry)> = Vec::new();
184    let mut at_entry = 0;
185    while at_entry < central.len() {
186        let fixed = central
187            .get(at_entry..at_entry + CENTRAL_LEN)
188            .filter(|fixed| fixed[..4] == CENTRAL)
189            .ok_or_else(|| not_a_bundle("its directory holds something that is not an entry"))?;
190        if short(fixed, 8) & ENCRYPTED != 0 {
191            return Err(not_a_bundle("it has an encrypted entry"));
192        }
193        let (name_len, extra_len, comment_len) = (
194            usize::from(short(fixed, 28)),
195            usize::from(short(fixed, 30)),
196            usize::from(short(fixed, 32)),
197        );
198        let name_at = at_entry + CENTRAL_LEN;
199        let extra_at = name_at + name_len;
200        let next = extra_at + extra_len + comment_len;
201        let (name, extra) = central
202            .get(name_at..extra_at)
203            .zip(central.get(extra_at..extra_at + extra_len))
204            .filter(|_| next <= central.len())
205            .ok_or_else(|| not_a_bundle("an entry of its directory runs past it"))?;
206        let name = std::str::from_utf8(name)
207            .map_err(|_| not_a_bundle("an entry's name is not UTF-8"))?
208            .to_owned();
209
210        let mut size = u64::from(long(fixed, 24));
211        let mut compressed = u64::from(long(fixed, 20));
212        let mut local = u64::from(long(fixed, 42));
213        let mut wide_values = zip64_values(extra)?.into_iter();
214        for value in [&mut size, &mut compressed, &mut local] {
215            if *value == u64::from(u32::MAX) {
216                *value = wide_values
217                    .next()
218                    .ok_or_else(|| not_a_bundle("an entry lacks its zip64 sizes"))?;
219            }
220        }
221        found.push((
222            name,
223            Entry {
224                range: local..local,
225                method: short(fixed, 10),
226                compressed,
227                size,
228                crc: long(fixed, 16),
229            },
230        ));
231        at_entry = next;
232    }
233    if found.len() as u64 != count {
234        return Err(not_a_bundle(
235            "its directory does not hold the entries it counts",
236        ));
237    }
238
239    // Each entry runs to where the next one starts, the last to the
240    // directory.
241    found.sort_by_key(|(_, entry)| entry.range.start);
242    let ends: Vec<u64> = found
243        .iter()
244        .skip(1)
245        .map(|(_, entry)| entry.range.start)
246        .chain([offset])
247        .collect();
248    let mut entries = BTreeMap::new();
249    for ((name, mut entry), end) in found.into_iter().zip(ends) {
250        if end < entry.range.start + LOCAL_HEADER_LEN as u64 + entry.compressed {
251            return Err(not_a_bundle("its entries overlap"));
252        }
253        entry.range.end = end;
254        if entries.insert(name, entry).is_some() {
255            return Err(not_a_bundle("it names one entry twice"));
256        }
257    }
258    Ok(Back::Directory(Directory {
259        len: file_len,
260        entries,
261    }))
262}
263
264/// The zip64 sizes and offset an entry's extra field holds, in the order
265/// they stand in for the classic ones.
266fn zip64_values(mut extra: &[u8]) -> Result<Vec<u64>, BundleError> {
267    while extra.len() >= 4 {
268        let (id, len) = (short(extra, 0), usize::from(short(extra, 2)));
269        let body = extra
270            .get(4..4 + len)
271            .ok_or_else(|| not_a_bundle("an entry's extra field runs past it"))?;
272        if id == ZIP64_EXTRA {
273            return Ok(body.chunks_exact(8).map(|word| wide(word, 0)).collect());
274        }
275        extra = &extra[4 + len..];
276    }
277    Ok(Vec::new())
278}
279
280impl Directory {
281    /// How long the bundle is.
282    pub fn file_len(&self) -> u64 {
283        self.len
284    }
285
286    /// The bytes that hold the head and the manifest: the two entries an
287    /// install reads before it knows which blobs it wants.
288    pub fn front(&self) -> Result<Range<u64>, BundleError> {
289        let head = self.entry(HEAD_ENTRY)?;
290        let manifest = self.entry(MANIFEST_ENTRY)?;
291        Ok(head.range.start.min(manifest.range.start)..head.range.end.max(manifest.range.end))
292    }
293
294    /// The manifest, from `front`, the bytes of [`Directory::front`]. Makes
295    /// the promises [`crate::open_bundle`] makes: the head is in this
296    /// reader's format, read before the manifest is, and every blob the
297    /// manifest names is in the bundle.
298    pub fn open(&self, front: &[u8]) -> Result<Manifest, BundleError> {
299        let base = self.front()?.start;
300        let entry_bytes = |name: &'static str| {
301            let entry = self.entry(name)?;
302            let raw = front
303                .get((entry.range.start - base) as usize..(entry.range.end - base) as usize)
304                .ok_or_else(|| not_a_bundle("its front is shorter than its directory says"))?;
305            entry.decode(raw)
306        };
307        parse_head(&entry_bytes(HEAD_ENTRY)?)?;
308        let manifest = parse_manifest(&entry_bytes(MANIFEST_ENTRY)?)?;
309        for id in manifest.blob_ids() {
310            self.blob(id)?;
311        }
312        Ok(manifest)
313    }
314
315    /// Where the blob `id` is.
316    pub fn blob(&self, id: &str) -> Result<&Entry, BundleError> {
317        self.entries
318            .get(&blob_entry(id))
319            .ok_or_else(|| BundleError::MissingBlob(id.to_owned()))
320    }
321
322    fn entry(&self, name: &'static str) -> Result<&Entry, BundleError> {
323        self.entries
324            .get(name)
325            .ok_or(BundleError::MissingEntry(name))
326    }
327}
328
329/// Where an [`EntryDecoder`] is in the bytes it is given.
330enum State<W: Write> {
331    /// The local header, gathered until its names and extras are in.
332    Header(Vec<u8>),
333    /// The data, this much of it still to come.
334    Data { left: u64, sink: Sink<W> },
335    /// Past the data: a trailer, or nothing.
336    Done(Checked<W>),
337}
338
339enum Sink<W: Write> {
340    Stored(Checked<W>),
341    Deflated(DeflateDecoder<Checked<W>>),
342}
343
344/// Counts and checksums what is written through it.
345struct Checked<W> {
346    inner: W,
347    crc: Crc,
348}
349
350impl<W: Write> Write for Checked<W> {
351    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
352        let written = self.inner.write(buf)?;
353        self.crc.update(&buf[..written]);
354        Ok(written)
355    }
356
357    fn flush(&mut self) -> io::Result<()> {
358        self.inner.flush()
359    }
360}
361
362/// Decodes an entry from the bytes of its range, as they arrive.
363pub struct EntryDecoder<W: Write> {
364    entry: Entry,
365    state: State<W>,
366    out: Option<W>,
367}
368
369fn invalid(why: &'static str) -> io::Error {
370    io::Error::new(io::ErrorKind::InvalidData, why)
371}
372
373impl<W: Write> EntryDecoder<W> {
374    /// Once the local header is whole: where the data starts in it, and the
375    /// state that reads the data the way the directory says.
376    fn begin(&mut self, header: &[u8]) -> io::Result<Option<(usize, State<W>)>> {
377        if header.len() < LOCAL_HEADER_LEN {
378            return Ok(None);
379        }
380        if header[..4] != LOCAL_HEADER {
381            return Err(invalid("an entry does not begin with its local header"));
382        }
383        let start =
384            LOCAL_HEADER_LEN + usize::from(short(header, 26)) + usize::from(short(header, 28));
385        if header.len() < start {
386            return Ok(None);
387        }
388        let out = Checked {
389            inner: self.out.take().expect("the sink is made once"),
390            crc: Crc::new(),
391        };
392        let sink = match self.entry.method {
393            STORED => Sink::Stored(out),
394            DEFLATED => Sink::Deflated(DeflateDecoder::new(out)),
395            _ => return Err(invalid("an entry is compressed in a way a bundle is not")),
396        };
397        let data = State::Data {
398            left: self.entry.compressed,
399            sink,
400        };
401        Ok(Some((start, data)))
402    }
403
404    /// Where the decoded bytes go, to take what has been written so far.
405    pub fn get_mut(&mut self) -> &mut W {
406        match &mut self.state {
407            State::Header(_) => self.out.as_mut().expect("the sink is made once"),
408            State::Data {
409                sink: Sink::Stored(out),
410                ..
411            }
412            | State::Done(out) => &mut out.inner,
413            State::Data {
414                sink: Sink::Deflated(out),
415                ..
416            } => &mut out.get_mut().inner,
417        }
418    }
419
420    /// Check what was written and hand back where it went.
421    pub fn finish(self) -> Result<W, BundleError> {
422        let checked = match self.state {
423            State::Done(checked) => checked,
424            State::Data { left: 0, sink } => finish_sink(sink)?,
425            _ => return Err(not_a_bundle("an entry ends before its data does")),
426        };
427        if checked.crc.amount() as u64 != self.entry.size & u64::from(u32::MAX)
428            || checked.crc.sum() != self.entry.crc
429        {
430            return Err(not_a_bundle(
431                "an entry does not decode to what its directory says",
432            ));
433        }
434        Ok(checked.inner)
435    }
436}
437
438fn finish_sink<W: Write>(sink: Sink<W>) -> io::Result<Checked<W>> {
439    match sink {
440        Sink::Stored(checked) => Ok(checked),
441        Sink::Deflated(decoder) => decoder.finish(),
442    }
443}
444
445impl<W: Write> Write for EntryDecoder<W> {
446    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
447        let mut rest = buf;
448        while !rest.is_empty() {
449            self.state = match std::mem::replace(&mut self.state, State::Header(Vec::new())) {
450                State::Header(mut header) => {
451                    let before = header.len();
452                    header.extend_from_slice(rest);
453                    match self.begin(&header)? {
454                        // What came in past the header is data.
455                        Some((start, data)) => {
456                            rest = &rest[start - before..];
457                            data
458                        }
459                        None => {
460                            rest = &[];
461                            State::Header(header)
462                        }
463                    }
464                }
465                State::Data { left, mut sink } => {
466                    let take = rest.len().min(usize::try_from(left).unwrap_or(usize::MAX));
467                    match &mut sink {
468                        Sink::Stored(out) => out.write_all(&rest[..take])?,
469                        Sink::Deflated(out) => out.write_all(&rest[..take])?,
470                    }
471                    rest = &rest[take..];
472                    match left - take as u64 {
473                        0 => State::Done(finish_sink(sink)?),
474                        left => State::Data { left, sink },
475                    }
476                }
477                // A data descriptor, which the directory already had.
478                done @ State::Done(_) => {
479                    rest = &[];
480                    done
481                }
482            };
483        }
484        Ok(buf.len())
485    }
486
487    fn flush(&mut self) -> io::Result<()> {
488        match &mut self.state {
489            State::Data {
490                sink: Sink::Stored(out),
491                ..
492            }
493            | State::Done(out) => out.flush(),
494            State::Data {
495                sink: Sink::Deflated(out),
496                ..
497            } => out.flush(),
498            State::Header(_) => Ok(()),
499        }
500    }
501}