Skip to main content

grit_lib/odb/store/
packs.rs

1//! Read-only pack-file object backend ([`PackedObjects`]).
2//!
3//! All pack cache and index reads run under the owning [`PackStore`](crate::pack_store::PackStore)
4//! context via [`PackStore::with_context`](crate::pack_store::PackStore::with_context).
5
6use std::collections::HashSet;
7use std::io::{self, Cursor, Read};
8use std::ops::ControlFlow;
9use std::path::Path;
10use std::sync::Arc;
11
12use flate2::{Decompress, FlushDecompress, Status};
13
14use crate::error::{Error, Result};
15use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
16use crate::pack::{self, read_local_pack_indexes_cached, reprepare_pack_directory_on_miss};
17use crate::pack_index::PackIndex;
18use crate::pack_store::PackStore;
19use crate::zlib_inflate::{LOOSE_INFLATE_INPUT_CHUNK, LOOSE_INFLATE_OUTPUT_CHUNK};
20
21use super::{ObjectStore, ObjectStream};
22
23/// Which local pack indexes participate in filtered membership checks.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct PackFilter {
26    /// When false, skip indexes with a sibling `.promisor` marker.
27    pub include_promisor: bool,
28    /// When false, skip indexes with a sibling `.mtimes` (cruft) marker.
29    pub include_cruft: bool,
30}
31
32impl Default for PackFilter {
33    fn default() -> Self {
34        Self {
35            include_promisor: false,
36            include_cruft: true,
37        }
38    }
39}
40
41/// Pack-index-backed read-only [`ObjectStore`] for one `objects/` directory.
42pub struct PackedObjects {
43    store: Arc<PackStore>,
44}
45
46impl std::fmt::Debug for PackedObjects {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("PackedObjects")
49            .field("objects_dir", &self.store.objects_dir())
50            .finish()
51    }
52}
53
54impl PackedObjects {
55    /// Open pack reads for `store`'s `objects/` directory.
56    #[must_use]
57    pub fn new(store: Arc<PackStore>) -> Self {
58        Self { store }
59    }
60
61    /// Shared pack cache handle (same as used for reads).
62    #[must_use]
63    pub fn pack_store(&self) -> &Arc<PackStore> {
64        &self.store
65    }
66
67    /// Object ids listed in a pack index file (for ingest paths outside the live ODB cache).
68    ///
69    /// # Errors
70    ///
71    /// Propagates pack index parse/verify failures from [`pack::read_pack_index`].
72    pub fn object_ids_from_index_path(idx_path: &Path) -> Result<HashSet<ObjectId>> {
73        let idx = pack::read_pack_index(idx_path)?;
74        let mut out = HashSet::new();
75        for entry in idx.iter() {
76            if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
77                out.insert(oid);
78            }
79        }
80        Ok(out)
81    }
82
83    /// Whether `oid` appears in any pack index matching `filter`.
84    ///
85    /// Used for local materialization checks (promisor/cruft exclusion); not part of
86    /// [`ObjectStore::contains`], which ignores promisor packs.
87    ///
88    /// # Errors
89    ///
90    /// Propagates pack directory enumeration failures.
91    pub fn contains_filtered(&self, oid: &ObjectId, filter: PackFilter) -> Result<bool> {
92        self.with_pack(|objects_dir| {
93            for idx in filtered_indexes(objects_dir, filter)? {
94                if idx.contains(oid)? {
95                    return Ok(true);
96                }
97            }
98            Ok(false)
99        })
100    }
101
102    /// Invoke `f` for each object id stored in a promisor-marked pack index.
103    ///
104    /// # Errors
105    ///
106    /// Propagates pack index enumeration failures.
107    pub fn for_each_promisor_object(
108        &self,
109        f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
110    ) -> Result<()> {
111        self.with_pack(|objects_dir| {
112            let indexes = read_local_pack_indexes_cached(objects_dir)?;
113            let mut seen = HashSet::new();
114            for idx in indexes {
115                if !idx.is_promisor {
116                    continue;
117                }
118                for entry in idx.iter() {
119                    let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
120                        continue;
121                    };
122                    if !seen.insert(oid) {
123                        continue;
124                    }
125                    if f(&oid).is_break() {
126                        return Ok(());
127                    }
128                }
129            }
130            Ok(())
131        })
132    }
133
134    /// Collect object ids from pack indexes that have a sibling `.keep` file.
135    ///
136    /// # Errors
137    ///
138    /// Propagates pack directory or index read failures.
139    pub fn kept_pack_object_ids(&self) -> Result<HashSet<ObjectId>> {
140        self.with_pack(collect_kept_pack_object_ids)
141    }
142
143    /// Invoke `f` for each object id in a local pack index without a sibling `.keep`
144    /// or `.promisor` file.
145    ///
146    /// Used by repack when retaining unreachable storage from packs that will be removed.
147    ///
148    /// # Errors
149    ///
150    /// Propagates pack index enumeration failures.
151    pub fn for_each_removable_pack_object(
152        &self,
153        f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
154    ) -> Result<()> {
155        self.with_pack(|objects_dir| for_each_removable_pack_object_ids(objects_dir, f))
156    }
157
158    /// Touch the mtime of the pack file holding `oid` (Git pack freshen), when not cruft.
159    ///
160    /// # Errors
161    ///
162    /// Propagates I/O failures from pack listing or utime.
163    pub fn freshen_pack_containing(&self, oid: &ObjectId) -> Result<bool> {
164        self.with_pack(|objects_dir| {
165            let indexes = read_local_pack_indexes_cached(objects_dir)?;
166            for idx in &indexes {
167                if !idx.contains(oid)? {
168                    continue;
169                }
170                if idx.is_cruft {
171                    return Ok(false);
172                }
173                return Ok(touch_pack_mtime(&idx.pack_path));
174            }
175            Ok(false)
176        })
177    }
178
179    fn with_pack<R>(&self, f: impl FnOnce(&std::path::Path) -> Result<R>) -> Result<R> {
180        if PackStore::read_context_matches_objects_dir(self.store.objects_dir()) {
181            return f(self.store.objects_dir());
182        }
183        PackStore::with_context(Arc::clone(&self.store), || f(self.store.objects_dir()))
184    }
185
186    fn hash_algo_from_indexes(&self) -> HashAlgo {
187        self.with_pack(|objects_dir| {
188            let Ok(indexes) = read_local_pack_indexes_cached(objects_dir) else {
189                return Ok(HashAlgo::Sha1);
190            };
191            Ok(indexes
192                .first()
193                .and_then(|idx| HashAlgo::from_len(idx.hash_bytes()))
194                .unwrap_or(HashAlgo::Sha1))
195        })
196        .unwrap_or(HashAlgo::Sha1)
197    }
198
199    fn read_from_packs(&self, oid: &ObjectId) -> Result<Option<Object>> {
200        self.with_pack(
201            |objects_dir| match pack::read_object_from_packs(objects_dir, oid) {
202                Ok(obj) => Ok(Some(obj)),
203                Err(Error::ObjectNotFound(_)) => Ok(None),
204                Err(err) => Err(err),
205            },
206        )
207    }
208
209    fn read_from_packs_into(
210        &self,
211        oid: &ObjectId,
212        buf: &mut Vec<u8>,
213    ) -> Result<Option<ObjectKind>> {
214        self.with_pack(|objects_dir| {
215            match pack::read_object_from_packs_into(objects_dir, oid, buf) {
216                Ok(kind) => Ok(Some(kind)),
217                Err(Error::ObjectNotFound(_)) => Ok(None),
218                Err(err) => Err(err),
219            }
220        })
221    }
222
223    fn read_info_from_packs(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
224        self.with_pack(
225            |objects_dir| match pack::read_object_info_from_packs(objects_dir, oid) {
226                Ok(info) => Ok(Some(info)),
227                Err(Error::ObjectNotFound(_)) => Ok(None),
228                Err(err) => Err(err),
229            },
230        )
231    }
232}
233
234impl ObjectStore for PackedObjects {
235    fn hash_algo(&self) -> HashAlgo {
236        self.hash_algo_from_indexes()
237    }
238
239    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
240        self.read_from_packs(oid)
241    }
242
243    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
244        self.read_from_packs_into(oid, buf)
245    }
246
247    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
248        self.read_info_from_packs(oid)
249    }
250
251    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
252        self.with_pack(|dir| {
253            let verify_crc = pack::count_local_packs_holding(dir, oid)? > 1;
254            let mut entry = pack::locate_packed_whole_object(dir, oid, verify_crc)?;
255            if matches!(entry, pack::PackedEntry::Absent) && reprepare_pack_directory_on_miss(dir)?
256            {
257                entry = pack::locate_packed_whole_object(dir, oid, verify_crc)?;
258            }
259            let pack::PackedEntry::Whole(whole) = entry else {
260                return stream_from_buffered_read(self, oid);
261            };
262            Ok(Some(ObjectStream {
263                kind: whole.kind,
264                size: whole.size,
265                reader: Box::new(PackPayloadStream::new(whole)),
266            }))
267        })
268    }
269
270    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
271        self.with_pack(|objects_dir| {
272            let indexes = read_local_pack_indexes_cached(objects_dir)?;
273            let mut seen = HashSet::new();
274            for idx in &indexes {
275                for entry in idx.iter() {
276                    let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
277                        continue;
278                    };
279                    if !seen.insert(oid) {
280                        continue;
281                    }
282                    if f(&oid).is_break() {
283                        return Ok(());
284                    }
285                }
286            }
287            Ok(())
288        })
289    }
290
291    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
292        if prefix.len() > self.hash_algo().hex_len() {
293            return Ok(());
294        }
295        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
296        self.with_pack(|objects_dir| {
297            let indexes = read_local_pack_indexes_cached(objects_dir)?;
298            let mut seen = HashSet::new();
299            let mut count = 0usize;
300            for idx in &indexes {
301                collect_prefix_from_index(idx, &prefix, limit, &mut count, out, &mut seen)?;
302                if limit != 0 && count >= limit {
303                    break;
304                }
305            }
306            Ok(())
307        })
308    }
309
310    fn refresh(&self) -> Result<bool> {
311        self.with_pack(reprepare_pack_directory_on_miss)
312    }
313}
314
315fn for_each_removable_pack_object_ids(
316    objects_dir: &Path,
317    f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
318) -> Result<()> {
319    let pack_dir = objects_dir.join("pack");
320    let indexes = read_local_pack_indexes_cached(objects_dir)?;
321    let mut seen = HashSet::new();
322    for idx in indexes {
323        if idx.is_promisor || pack_index_has_keep_marker(&pack_dir, &idx.pack_path) {
324            continue;
325        }
326        let hash_bytes = idx.hash_bytes();
327        for entry in idx.iter() {
328            if entry.oid().len() != hash_bytes {
329                continue;
330            }
331            let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
332                continue;
333            };
334            if !seen.insert(oid) {
335                continue;
336            }
337            if f(&oid).is_break() {
338                return Ok(());
339            }
340        }
341    }
342    Ok(())
343}
344
345fn pack_index_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
346    let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
347        return false;
348    };
349    pack_dir.join(format!("{stem}.keep")).is_file()
350}
351
352fn collect_kept_pack_object_ids(objects_dir: &Path) -> Result<HashSet<ObjectId>> {
353    let pack_dir = objects_dir.join("pack");
354    let mut kept = HashSet::new();
355    if !pack_dir.is_dir() {
356        return Ok(kept);
357    }
358    for entry in std::fs::read_dir(&pack_dir).map_err(Error::Io)? {
359        let entry = entry.map_err(Error::Io)?;
360        let path = entry.path();
361        if path.extension().is_some_and(|ext| ext == "keep") {
362            let idx_path = path.with_extension("idx");
363            if idx_path.is_file() {
364                if let Ok(oids) = pack::read_idx_object_ids(&idx_path) {
365                    kept.extend(oids);
366                }
367            }
368        }
369    }
370    Ok(kept)
371}
372
373fn filtered_indexes(
374    objects_dir: &std::path::Path,
375    filter: PackFilter,
376) -> Result<Vec<Arc<PackIndex>>> {
377    let indexes = read_local_pack_indexes_cached(objects_dir)?;
378    Ok(indexes
379        .into_iter()
380        .filter(|idx| {
381            (filter.include_promisor || !idx.is_promisor) && (filter.include_cruft || !idx.is_cruft)
382        })
383        .collect())
384}
385
386fn collect_prefix_from_index(
387    idx: &PackIndex,
388    prefix: &ObjectIdPrefix,
389    limit: usize,
390    count: &mut usize,
391    out: &mut Vec<ObjectId>,
392    seen: &mut HashSet<ObjectId>,
393) -> Result<()> {
394    let lo = lower_bound_oid(idx, prefix.lowest().as_bytes());
395    let hi = upper_bound_oid(idx, prefix.highest().as_bytes());
396    for i in lo..hi {
397        let digest = idx.oid_at(i);
398        if !prefix.matches_digest(digest) {
399            continue;
400        }
401        let Ok(oid) = ObjectId::from_bytes(digest) else {
402            continue;
403        };
404        if !seen.insert(oid) {
405            continue;
406        }
407        out.push(oid);
408        *count += 1;
409        if limit != 0 && *count >= limit {
410            break;
411        }
412    }
413    Ok(())
414}
415
416fn lower_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
417    let mut lo = 0usize;
418    let mut hi = idx.len();
419    while lo < hi {
420        let mid = lo + (hi - lo) / 2;
421        if idx.oid_at(mid) < needle {
422            lo = mid + 1;
423        } else {
424            hi = mid;
425        }
426    }
427    lo
428}
429
430fn upper_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
431    let mut lo = 0usize;
432    let mut hi = idx.len();
433    while lo < hi {
434        let mid = lo + (hi - lo) / 2;
435        if idx.oid_at(mid) <= needle {
436            lo = mid + 1;
437        } else {
438            hi = mid;
439        }
440    }
441    lo
442}
443
444/// Freshen `pack_path` (like rewriting objects it already holds) so age-based pruning
445/// treats its objects as new. Returns `false` when the times cannot be set.
446pub(crate) fn touch_pack_mtime(pack_path: &std::path::Path) -> bool {
447    let Some(touched) = crate::fs_readonly::set_file_times_now(pack_path) else {
448        return false;
449    };
450    pack::refresh_pack_bytes_signature(pack_path, touched);
451    true
452}
453
454/// Compressed bytes a [`PackPayloadStream`] reads from a mapped pack before it drops the
455/// mapping's pages from the resident set, so streaming a large object keeps a bounded
456/// window of the pack resident instead of the whole compressed entry.
457const STREAM_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
458
459/// Inflates a whole (non-delta) pack entry straight out of the mapped pack.
460///
461/// Yields exactly the declared payload size, then requires the zlib stream to end there, so
462/// truncated, oversized or checksum-corrupt entries surface as read errors.
463struct PackPayloadStream {
464    whole: pack::PackedWholeObject,
465    dec: Decompress,
466    pos: usize,
467    remaining: u64,
468    stream_ended: bool,
469    /// Compressed bytes consumed since the mapping's pages were last released.
470    resident: usize,
471    /// Pack-index CRC32 check deferred until the compressed entry has been consumed.
472    index_crc: Option<(u32, crc32fast::Hasher)>,
473}
474
475impl PackPayloadStream {
476    fn new(whole: pack::PackedWholeObject) -> Self {
477        let entry_start = whole.entry_offset as usize;
478        let index_crc = whole.index_crc.map(|expected| {
479            let mut hasher = crc32fast::Hasher::new();
480            hasher.update(&whole.pack[entry_start..whole.zlib_offset]);
481            (expected, hasher)
482        });
483        Self {
484            pos: whole.zlib_offset,
485            remaining: whole.size,
486            whole,
487            dec: Decompress::new(true),
488            stream_ended: false,
489            resident: 0,
490            index_crc,
491        }
492    }
493
494    fn note_compressed_consumed(&mut self, start: usize, end: usize) {
495        if let Some((_, hasher)) = &mut self.index_crc {
496            hasher.update(&self.whole.pack[start..end]);
497        }
498    }
499
500    fn verify_index_crc_if_due(&mut self) -> io::Result<()> {
501        if self.pos < self.whole.entry_end {
502            return Ok(());
503        }
504        let Some((expected, hasher)) = self.index_crc.take() else {
505            return Ok(());
506        };
507        if hasher.finalize() != expected {
508            return Err(corrupt_pack_stream("pack index CRC mismatch"));
509        }
510        Ok(())
511    }
512
513    /// Inflate into `out`, returning the bytes produced.
514    fn inflate(&mut self, out: &mut [u8]) -> io::Result<usize> {
515        let cap = out.len().min(LOOSE_INFLATE_OUTPUT_CHUNK);
516        let out = &mut out[..cap];
517        let input_end = self.whole.data_end.min(self.whole.entry_end);
518        if self.pos >= input_end {
519            if self.stream_ended {
520                return Ok(0);
521            }
522            return Err(corrupt_pack_stream("zlib stream truncated"));
523        }
524        let input_take = (input_end - self.pos).min(LOOSE_INFLATE_INPUT_CHUNK);
525        let input = &self.whole.pack[self.pos..self.pos + input_take];
526        let before_in = self.dec.total_in();
527        let before_out = self.dec.total_out();
528        let status = self
529            .dec
530            .decompress(input, out, FlushDecompress::None)
531            .map_err(|e| corrupt_pack_stream(&e.to_string()))?;
532        let consumed = (self.dec.total_in() - before_in) as usize;
533        let produced = (self.dec.total_out() - before_out) as usize;
534        let from = self.pos;
535        self.pos += consumed;
536        self.note_compressed_consumed(from, self.pos);
537        self.resident += consumed;
538        if self.resident >= STREAM_RESIDENT_PACK_BYTES {
539            self.whole.pack.release_resident_through(self.pos);
540            self.resident = 0;
541        }
542        if status == Status::StreamEnd {
543            self.stream_ended = true;
544        } else if consumed == 0 && produced == 0 {
545            return Err(corrupt_pack_stream("zlib stream truncated"));
546        }
547        Ok(produced)
548    }
549}
550
551fn corrupt_pack_stream(detail: &str) -> io::Error {
552    io::Error::new(
553        io::ErrorKind::InvalidData,
554        format!("corrupt packed object: {detail}"),
555    )
556}
557
558impl Read for PackPayloadStream {
559    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
560        if self.remaining == 0 {
561            let mut excess = [0u8; 1];
562            while !self.stream_ended {
563                if self.inflate(&mut excess)? > 0 {
564                    return Err(corrupt_pack_stream("more data than declared size"));
565                }
566            }
567            while self.pos < self.whole.entry_end {
568                let take = (self.whole.entry_end - self.pos).min(LOOSE_INFLATE_INPUT_CHUNK);
569                let from = self.pos;
570                self.pos += take;
571                self.note_compressed_consumed(from, self.pos);
572            }
573            self.verify_index_crc_if_due()?;
574            return Ok(0);
575        }
576        if buf.is_empty() {
577            return Ok(0);
578        }
579        let max = usize::try_from(self.remaining).map_or(buf.len(), |r| r.min(buf.len()));
580        loop {
581            if self.stream_ended {
582                return Err(corrupt_pack_stream("less data than declared size"));
583            }
584            let produced = self.inflate(&mut buf[..max])?;
585            if produced > 0 {
586                self.remaining -= produced as u64;
587                return Ok(produced);
588            }
589        }
590    }
591}
592
593fn stream_from_buffered_read(
594    store: &PackedObjects,
595    oid: &ObjectId,
596) -> Result<Option<ObjectStream<'static>>> {
597    let Some(obj) = store.read(oid)? else {
598        return Ok(None);
599    };
600    let size = u64::try_from(obj.data.len())
601        .map_err(|_| Error::CorruptObject(format!("object size overflow for {}", oid.to_hex())))?;
602    Ok(Some(ObjectStream {
603        kind: obj.kind,
604        size,
605        reader: Box::new(Cursor::new(obj.data)),
606    }))
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    use flate2::write::ZlibEncoder;
613    use flate2::Compression;
614    use std::io::Write;
615
616    use crate::pack_map::PackData;
617
618    const TRAILER: usize = 20;
619
620    fn zlib(data: &[u8]) -> Vec<u8> {
621        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
622        enc.write_all(data).expect("deflate");
623        enc.finish().expect("deflate")
624    }
625
626    /// A fake pack holding one whole entry: some leading bytes, the zlib stream, a trailer.
627    fn whole(zlib_stream: &[u8], declared_size: u64) -> pack::PackedWholeObject {
628        let mut bytes = b"PACK\0\0\0\x02\0\0\0\x01\xb0".to_vec();
629        let zlib_offset = bytes.len();
630        bytes.extend_from_slice(zlib_stream);
631        let data_end = bytes.len();
632        bytes.extend_from_slice(&[0u8; TRAILER]);
633        pack::PackedWholeObject {
634            pack: PackData::from_owned(bytes),
635            entry_offset: 12,
636            entry_end: data_end,
637            zlib_offset,
638            data_end,
639            kind: ObjectKind::Blob,
640            size: declared_size,
641            index_crc: None,
642        }
643    }
644
645    fn read_all(whole: pack::PackedWholeObject) -> io::Result<Vec<u8>> {
646        let mut stream = PackPayloadStream::new(whole);
647        let mut out = Vec::new();
648        let mut chunk = [0u8; 1000];
649        loop {
650            match stream.read(&mut chunk)? {
651                0 => return Ok(out),
652                n => out.extend_from_slice(&chunk[..n]),
653            }
654        }
655    }
656
657    fn data() -> Vec<u8> {
658        (0..200_000u32)
659            .flat_map(|i| (i % 251).to_le_bytes())
660            .collect()
661    }
662
663    #[test]
664    fn pack_payload_stream_yields_the_whole_payload() {
665        let data = data();
666        let out = read_all(whole(&zlib(&data), data.len() as u64)).expect("stream");
667        assert!(out == data);
668    }
669
670    #[test]
671    fn pack_payload_stream_rejects_truncated_entry() {
672        let data = data();
673        let stream = zlib(&data);
674        let err =
675            read_all(whole(&stream[..stream.len() / 2], data.len() as u64)).expect_err("truncated");
676        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
677    }
678
679    #[test]
680    fn pack_payload_stream_rejects_size_mismatch_both_ways() {
681        let data = data();
682        let stream = zlib(&data);
683        let err = read_all(whole(&stream, data.len() as u64 + 1)).expect_err("short");
684        assert!(err.to_string().contains("less data"), "{err}");
685        let err = read_all(whole(&stream, data.len() as u64 - 1)).expect_err("long");
686        assert!(err.to_string().contains("more data"), "{err}");
687    }
688
689    #[test]
690    fn pack_payload_stream_rejects_bad_checksum() {
691        let data = data();
692        let mut stream = zlib(&data);
693        let last = stream.len() - 1;
694        stream[last] ^= 0xff;
695        let err = read_all(whole(&stream, data.len() as u64)).expect_err("adler mismatch");
696        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
697    }
698}