Skip to main content

grit_lib/odb/store/
files.rs

1//! On-disk object source for one `objects/` directory (MIDX, packs, loose).
2
3use std::collections::HashSet;
4use std::ops::ControlFlow;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use crate::diagnostics::DiagnosticsHandle;
9use crate::error::{Error, Result};
10use crate::hash;
11use crate::midx::midx_oid_listed_in_tip;
12use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
13use crate::pack::{self, PackLookupOptions};
14use crate::pack_store::PackStore;
15
16use super::{
17    LocalSizedRead, LooseStore, MidxObjects, MidxObjectsStatus, ObjectStore, ObjectStream,
18    PackedObjects, WritableObjectStore,
19};
20use crate::odb::{empty_tree_oid_matches, WriteOptions};
21
22/// Read/write object storage backed by one `objects/` directory tree.
23pub struct FilesSource {
24    objects_dir: PathBuf,
25    loose: LooseStore,
26    packs: PackedObjects,
27    midx: MidxObjects,
28    use_midx: bool,
29    midx_tip_listing_for_exists: bool,
30    _diagnostics: DiagnosticsHandle,
31}
32
33impl std::fmt::Debug for FilesSource {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("FilesSource")
36            .field("objects_dir", &self.objects_dir)
37            .field("use_midx", &self.use_midx)
38            .finish()
39    }
40}
41
42impl FilesSource {
43    fn with_pack_access<R>(&self, f: impl FnOnce() -> R) -> R {
44        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
45            f()
46        } else if PackStore::has_context() {
47            pack::with_alternate_pack_store(self.packs.pack_store().clone(), f)
48        } else {
49            PackStore::with_context(self.packs.pack_store().clone(), f)
50        }
51    }
52
53    fn skip_loose_probe_after_empty_warmup(&self, oid: &ObjectId) -> bool {
54        pack::pack_cache::trusted_verified_empty_pack_listing(&self.objects_dir)
55            && self.loose.cached_absent_loose_path(oid)
56    }
57
58    /// Open a files source for `pack_store`'s directory.
59    ///
60    /// `use_midx` enables multi-pack-index reads when a chain is present.
61    /// `midx_tip_listing_for_exists` matches Git's MIDX membership probe for [`ObjectStore::contains`].
62    ///
63    /// # Errors
64    ///
65    /// Propagates [`MidxObjects::new`] failures other than a missing index.
66    pub fn open(
67        pack_store: Arc<PackStore>,
68        loose: LooseStore,
69        use_midx: bool,
70        midx_tip_listing_for_exists: bool,
71        diagnostics: DiagnosticsHandle,
72    ) -> Result<Self> {
73        let midx = MidxObjects::new(Arc::clone(&pack_store), use_midx)?;
74        Ok(Self {
75            objects_dir: pack_store.objects_dir().to_path_buf(),
76            loose,
77            packs: PackedObjects::new(pack_store),
78            midx,
79            use_midx,
80            midx_tip_listing_for_exists,
81            _diagnostics: diagnostics,
82        })
83    }
84
85    /// `objects/` root for this source.
86    #[must_use]
87    pub fn objects_dir(&self) -> &Path {
88        &self.objects_dir
89    }
90
91    /// Loose-object backend (writes target this store).
92    #[must_use]
93    pub fn loose_store(&self) -> &LooseStore {
94        &self.loose
95    }
96
97    /// Shared pack read backend.
98    #[must_use]
99    pub fn packed_objects(&self) -> &PackedObjects {
100        &self.packs
101    }
102
103    /// Whether `oid` is materialized under this `objects/` tree (local loose or non-promisor pack).
104    #[must_use]
105    pub fn exists_local_materialized(&self, oid: &ObjectId) -> bool {
106        super::local_object_materialized(&self.objects_dir, oid)
107    }
108
109    /// Resolve duplicate-write short-circuit for this directory before creating a loose file.
110    ///
111    /// Returns `Some(oid)` when the object is already present locally and was freshened when
112    /// appropriate. When `options.trust_new_loose` is set, only an existing loose file stops the
113    /// write (callers may still consult alternates).
114    pub fn try_finish_duplicate_write(
115        &self,
116        oid: &ObjectId,
117        options: WriteOptions,
118    ) -> Result<Option<ObjectId>> {
119        if let Some(existing) = self.loose.try_finish_if_loose_present(oid, options)? {
120            return Ok(Some(existing));
121        }
122        if options.trust_new_loose {
123            return Ok(None);
124        }
125        if options.assume_loose_only_existence && self.exists_local_materialized(oid) {
126            if self.freshen(oid)? {
127                return Ok(Some(*oid));
128            }
129            // Cruft packs (and other unfreshenable local copies) must not block loose
130            // materialization — matches `try_freshen_existing_local` / unpack-objects.
131            return Ok(None);
132        }
133        Ok(None)
134    }
135
136    /// Write zlib-compressed store bytes for a precomputed `oid` into this source's loose store.
137    ///
138    /// # Errors
139    ///
140    /// Same as [`WritableObjectStore::write`].
141    pub fn write_zlib_prehashed(
142        &self,
143        oid: &ObjectId,
144        zlib_store: &[u8],
145        options: WriteOptions,
146    ) -> Result<ObjectId> {
147        if let Some(existing) = self.try_finish_duplicate_write(oid, options)? {
148            return Ok(existing);
149        }
150        self.loose.write_zlib_prehashed(oid, zlib_store, options)
151    }
152
153    /// Whether a loose object file exists but could not be parsed (caller may consult alternates).
154    pub fn local_loose_exists_but_unreadable(&self, oid: &ObjectId, err: &Error) -> bool {
155        if !self.loose.object_path(oid).is_file() {
156            return false;
157        }
158        matches!(
159            err,
160            Error::CorruptObject(_)
161                | Error::CorruptLooseObject { .. }
162                | Error::LooseHashMismatch { .. }
163                | Error::Zlib(_)
164        )
165    }
166
167    fn midx_pack_filter_active(&self) -> bool {
168        self.use_midx && self.midx.status() == MidxObjectsStatus::Active
169    }
170
171    /// Whether an on-disk multi-pack-index chain drives reads for this `objects/` tree.
172    pub(crate) fn midx_reads_active(&self) -> bool {
173        self.midx_pack_filter_active()
174    }
175
176    fn midx_read_should_fallback_to_packs(err: &Error) -> bool {
177        err.is_damaged_object()
178    }
179
180    fn try_read_object_into(
181        &self,
182        oid: &ObjectId,
183        populate_delta_lru: bool,
184        buf: &mut Vec<u8>,
185    ) -> Result<Option<ObjectKind>> {
186        if empty_tree_oid_matches(self.hash_algo(), oid) {
187            buf.clear();
188            return Ok(Some(ObjectKind::Tree));
189        }
190
191        let midx_pack_filter = self.midx_pack_filter_active();
192        let pack_opts = PackLookupOptions {
193            skip_midx_covered_packs: midx_pack_filter,
194            only_midx_covered_packs: false,
195        };
196
197        if self.use_midx {
198            match self.midx.read_into(oid, buf) {
199                Ok(Some(kind)) => return Ok(Some(kind)),
200                Ok(None) => {}
201                Err(Error::Midx(_)) => {}
202                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
203                Err(err) => return Err(err),
204            }
205        }
206
207        match pack::try_read_object_from_packs_with_options_into(
208            &self.objects_dir,
209            oid,
210            pack_opts,
211            populate_delta_lru,
212            buf,
213        ) {
214            Ok(kind) => return Ok(Some(kind)),
215            Err(Error::ObjectNotFound(_)) => {}
216            Err(err) => return Err(err),
217        }
218
219        if midx_pack_filter {
220            match pack::try_read_object_from_packs_with_options_into(
221                &self.objects_dir,
222                oid,
223                PackLookupOptions::MIDX_COVERED_PACKS,
224                populate_delta_lru,
225                buf,
226            ) {
227                Ok(kind) => return Ok(Some(kind)),
228                Err(Error::ObjectNotFound(_)) => {}
229                Err(err) => return Err(err),
230            }
231        }
232
233        if self.skip_loose_probe_after_empty_warmup(oid) {
234            return Ok(None);
235        }
236        #[cfg(test)]
237        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
238        self.loose.read_into(oid, buf)
239    }
240
241    fn try_read_object(&self, oid: &ObjectId) -> Result<Option<Object>> {
242        if empty_tree_oid_matches(self.hash_algo(), oid) {
243            return Ok(Some(Object {
244                kind: ObjectKind::Tree,
245                data: Vec::new(),
246            }));
247        }
248
249        let midx_pack_filter = self.midx_pack_filter_active();
250        let pack_opts = PackLookupOptions {
251            skip_midx_covered_packs: midx_pack_filter,
252            only_midx_covered_packs: false,
253        };
254
255        if self.use_midx {
256            match self.midx.read(oid) {
257                Ok(Some(obj)) => return Ok(Some(obj)),
258                Ok(None) => {}
259                Err(Error::Midx(_)) => {}
260                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
261                Err(err) => return Err(err),
262            }
263        }
264
265        match pack::try_read_object_from_packs_with_options(&self.objects_dir, oid, pack_opts) {
266            Ok(obj) => return Ok(Some(obj)),
267            Err(Error::ObjectNotFound(_)) => {}
268            Err(err) => return Err(err),
269        }
270
271        if midx_pack_filter {
272            match pack::try_read_object_from_packs_with_options(
273                &self.objects_dir,
274                oid,
275                PackLookupOptions::MIDX_COVERED_PACKS,
276            ) {
277                Ok(obj) => return Ok(Some(obj)),
278                Err(Error::ObjectNotFound(_)) => {}
279                Err(err) => return Err(err),
280            }
281        }
282
283        if self.skip_loose_probe_after_empty_warmup(oid) {
284            return Ok(None);
285        }
286        #[cfg(test)]
287        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
288        self.loose.read(oid)
289    }
290
291    fn try_read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
292        if empty_tree_oid_matches(self.hash_algo(), oid) {
293            return Ok(Some(ObjectInfo {
294                kind: ObjectKind::Tree,
295                size: 0,
296            }));
297        }
298
299        let midx_pack_filter = self.midx_pack_filter_active();
300        let pack_opts = PackLookupOptions {
301            skip_midx_covered_packs: midx_pack_filter,
302            only_midx_covered_packs: false,
303        };
304
305        if self.use_midx {
306            match self.midx.read_info(oid) {
307                Ok(Some(info)) => return Ok(Some(info)),
308                Ok(None) => {}
309                Err(Error::Midx(_)) => {}
310                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
311                Err(err) => return Err(err),
312            }
313        }
314
315        match pack::try_read_object_info_from_packs_with_options(&self.objects_dir, oid, pack_opts)
316        {
317            Ok(info) => return Ok(Some(info)),
318            Err(Error::ObjectNotFound(_)) => {}
319            Err(err) => return Err(err),
320        }
321
322        if midx_pack_filter {
323            match pack::try_read_object_info_from_packs_with_options(
324                &self.objects_dir,
325                oid,
326                PackLookupOptions::MIDX_COVERED_PACKS,
327            ) {
328                Ok(info) => return Ok(Some(info)),
329                Err(Error::ObjectNotFound(_)) => {}
330                Err(err) => return Err(err),
331            }
332        }
333
334        if self.skip_loose_probe_after_empty_warmup(oid) {
335            return Ok(None);
336        }
337        #[cfg(test)]
338        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
339        self.loose.read_info(oid)
340    }
341
342    /// Find `oid` locally, reading it only when it is a loose object of at most `limit` bytes.
343    ///
344    /// Packed objects are not read here ([`LocalSizedRead::Packed`]); one stored whole is sized
345    /// from its entry header without inflating it. Loose objects are opened once, and inflation
346    /// stops at the header when the object is too large.
347    ///
348    /// # Errors
349    ///
350    /// Propagates pack lookup and loose read failures.
351    pub(crate) fn read_unless_larger(&self, oid: &ObjectId, limit: u64) -> Result<LocalSizedRead> {
352        let probe = || {
353            match pack::locate_packed_whole_object(&self.objects_dir, oid, false)? {
354                pack::PackedEntry::Whole(whole) => {
355                    return Ok(LocalSizedRead::Packed(Some(whole.size)))
356                }
357                pack::PackedEntry::NotWhole => return Ok(LocalSizedRead::Packed(None)),
358                pack::PackedEntry::Absent => {}
359            }
360            Ok(match self.loose.read_unless_larger(oid, limit)? {
361                Some(read) => LocalSizedRead::Loose(read),
362                None => LocalSizedRead::Missing,
363            })
364        };
365        self.with_pack_access(probe)
366    }
367
368    fn read_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<Object>> {
369        match self.try_read_object(oid) {
370            Ok(Some(obj)) => Ok(Some(obj)),
371            Ok(None) => {
372                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
373                    self.try_read_object(oid)
374                } else {
375                    Ok(None)
376                }
377            }
378            Err(err) => Err(err),
379        }
380    }
381
382    fn read_with_pack_retry_into(
383        &self,
384        oid: &ObjectId,
385        populate_delta_lru: bool,
386        buf: &mut Vec<u8>,
387    ) -> Result<Option<ObjectKind>> {
388        match self.try_read_object_into(oid, populate_delta_lru, buf) {
389            Ok(Some(kind)) => Ok(Some(kind)),
390            Ok(None) => {
391                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
392                    self.try_read_object_into(oid, populate_delta_lru, buf)
393                } else {
394                    Ok(None)
395                }
396            }
397            Err(err) => Err(err),
398        }
399    }
400
401    fn read_info_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
402        match self.try_read_info(oid) {
403            Ok(Some(info)) => Ok(Some(info)),
404            Ok(None) => {
405                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
406                    self.try_read_info(oid)
407                } else {
408                    Ok(None)
409                }
410            }
411            Err(err) => Err(err),
412        }
413    }
414
415    fn contains_in_packs(&self, oid: &ObjectId) -> Result<bool> {
416        let indexes = pack::read_local_pack_indexes_cached(&self.objects_dir)?;
417        if indexes.is_empty() && !pack_directory_has_index_files(&self.objects_dir) {
418            if self.midx_tip_listing_for_exists && self.use_midx {
419                match midx_oid_listed_in_tip(&self.objects_dir, oid) {
420                    Ok(Some(true)) => return Ok(true),
421                    Ok(Some(false)) | Ok(None) => {}
422                    Err(_) => return Ok(false),
423                }
424            }
425            return Ok(false);
426        }
427        let filter = super::PackFilter {
428            include_promisor: true,
429            include_cruft: true,
430        };
431        if self.packs.contains_filtered(oid, filter)? {
432            return Ok(true);
433        }
434        if pack::reprepare_pack_directory_on_miss(&self.objects_dir)?
435            && self.packs.contains_filtered(oid, filter)?
436        {
437            return Ok(true);
438        }
439        if self.midx_tip_listing_for_exists && self.use_midx {
440            match midx_oid_listed_in_tip(&self.objects_dir, oid) {
441                Ok(Some(true)) => return Ok(true),
442                Ok(Some(false)) | Ok(None) => {}
443                Err(_) => return Ok(false),
444            }
445        }
446        Ok(false)
447    }
448}
449
450impl FilesSource {
451    /// [`ObjectStore::read_into`], keeping delta bases resolved from packs in the delta-base
452    /// cache when `populate_delta_lru` is set.
453    pub(crate) fn read_into_with_delta_policy(
454        &self,
455        oid: &ObjectId,
456        populate_delta_lru: bool,
457        buf: &mut Vec<u8>,
458    ) -> Result<Option<ObjectKind>> {
459        self.with_pack_access(|| self.read_with_pack_retry_into(oid, populate_delta_lru, buf))
460    }
461
462    /// See [`crate::odb::Odb::read_packed_commit_header_block_into`].
463    pub(crate) fn read_packed_commit_header_block_into(
464        &self,
465        oid: &ObjectId,
466        buf: &mut Vec<u8>,
467    ) -> Result<bool> {
468        self.with_pack_access(|| {
469            pack::read_packed_commit_header_block_into(&self.objects_dir, oid, buf)
470        })
471    }
472
473    /// See [`crate::odb::Odb::read_packed_commit_header_blocks`].
474    pub(crate) fn read_packed_commit_header_blocks(
475        &self,
476        oids: &[ObjectId],
477        found: impl FnMut(usize, &[u8]),
478    ) -> Result<()> {
479        self.with_pack_access(|| {
480            pack::read_packed_commit_header_blocks(&self.objects_dir, oids, found)
481        })
482    }
483}
484
485impl ObjectStore for FilesSource {
486    fn hash_algo(&self) -> HashAlgo {
487        ObjectStore::hash_algo(&self.loose)
488    }
489
490    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
491        self.with_pack_access(|| self.read_with_pack_retry(oid))
492    }
493
494    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
495        self.read_into_with_delta_policy(oid, false, buf)
496    }
497
498    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
499        self.with_pack_access(|| self.read_info_with_pack_retry(oid))
500    }
501
502    fn contains(&self, oid: &ObjectId) -> Result<bool> {
503        if empty_tree_oid_matches(self.hash_algo(), oid) {
504            return Ok(true);
505        }
506        if self.loose.object_path(oid).is_file() {
507            return Ok(true);
508        }
509        self.with_pack_access(|| self.contains_in_packs(oid))
510    }
511
512    fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
513        // Batches are mostly packed objects (cached trees, unchanged tree members), so
514        // the loaded pack indexes answer first and only misses pay a loose-path stat. A
515        // pack failure falls through to `contains`, which reports it as a single probe would.
516        let filter = super::PackFilter {
517            include_promisor: true,
518            include_cruft: true,
519        };
520        let probe = || {
521            oids.iter()
522                .map(|oid| {
523                    if self.packs.contains_filtered(oid, filter).unwrap_or(false) {
524                        return Ok(true);
525                    }
526                    self.contains(oid)
527                })
528                .collect()
529        };
530        self.with_pack_access(probe)
531    }
532
533    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
534        if empty_tree_oid_matches(self.hash_algo(), oid) {
535            return Ok(Some(ObjectStream {
536                kind: ObjectKind::Tree,
537                size: 0,
538                reader: Box::new(std::io::Cursor::new(Vec::<u8>::new())),
539            }));
540        }
541        self.with_pack_access(|| {
542            if self.use_midx {
543                match self.midx.open_stream(oid) {
544                    Ok(Some(stream)) => return Ok(Some(stream)),
545                    Ok(None) => {}
546                    Err(Error::Midx(_)) => {}
547                    Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
548                    Err(err) => return Err(err),
549                }
550            }
551            match self.packs.open_stream(oid) {
552                Ok(Some(stream)) => return Ok(Some(stream)),
553                Ok(None) | Err(Error::ObjectNotFound(_)) => {}
554                Err(err) => return Err(err),
555            }
556            self.loose.open_stream(oid)
557        })
558    }
559
560    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
561        let mut seen = HashSet::new();
562        self.with_pack_access(|| {
563            let mut layer = |store: &dyn ObjectStore| -> Result<bool> {
564                super::for_each_propagate_break(store, &mut |oid| {
565                    if !seen.insert(*oid) {
566                        return ControlFlow::Continue(());
567                    }
568                    f(oid)
569                })
570            };
571            if self.use_midx && layer(&self.midx)? {
572                return Ok(());
573            }
574            if layer(&self.packs)? {
575                return Ok(());
576            }
577            let _ = layer(&self.loose)?;
578            Ok(())
579        })
580    }
581
582    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
583        ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
584        let mut seen: HashSet<ObjectId> = out.iter().copied().collect();
585        self.with_pack_access(|| files_lookup_prefix_layers(self, prefix, limit, out, &mut seen))
586    }
587
588    fn refresh(&self) -> Result<bool> {
589        self.with_pack_access(|| {
590            let mut changed = self.packs.refresh()?;
591            if self.midx.refresh()? {
592                changed = true;
593            }
594            Ok(changed)
595        })
596    }
597}
598
599impl WritableObjectStore for FilesSource {
600    fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId> {
601        let oid = hash::hash_object(self.hash_algo(), kind, data)?;
602        if let Some(existing) = self.try_finish_duplicate_write(&oid, options)? {
603            return Ok(existing);
604        }
605        self.loose.write(kind, data, options)
606    }
607
608    fn freshen(&self, oid: &ObjectId) -> Result<bool> {
609        if self.loose.freshen(oid)? {
610            return Ok(true);
611        }
612        self.with_pack_access(|| self.packs.freshen_pack_containing(oid))
613    }
614}
615
616fn pack_directory_has_index_files(objects_dir: &Path) -> bool {
617    let pack_dir = objects_dir.join("pack");
618    let Ok(entries) = std::fs::read_dir(pack_dir) else {
619        return false;
620    };
621    entries
622        .filter_map(|e| e.ok())
623        .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
624}
625
626fn files_lookup_prefix_layers(
627    src: &FilesSource,
628    prefix: &str,
629    limit: usize,
630    out: &mut Vec<ObjectId>,
631    seen: &mut HashSet<ObjectId>,
632) -> Result<()> {
633    if src.use_midx {
634        merge_prefix_matches(&src.midx, prefix, limit, out, seen)?;
635        if limit != 0 && out.len() >= limit {
636            return Ok(());
637        }
638    }
639    merge_prefix_matches(&src.packs, prefix, limit, out, seen)?;
640    if limit != 0 && out.len() >= limit {
641        return Ok(());
642    }
643    merge_prefix_matches(&src.loose, prefix, limit, out, seen)
644}
645
646fn merge_prefix_matches(
647    store: &dyn ObjectStore,
648    prefix: &str,
649    limit: usize,
650    out: &mut Vec<ObjectId>,
651    seen: &mut HashSet<ObjectId>,
652) -> Result<()> {
653    if limit != 0 && out.len() >= limit {
654        return Ok(());
655    }
656    if limit == 0 {
657        let mut scratch = Vec::new();
658        store.lookup_prefix(prefix, 0, &mut scratch)?;
659        for oid in scratch {
660            if seen.insert(oid) {
661                out.push(oid);
662            }
663        }
664        return Ok(());
665    }
666
667    let goal = limit;
668    let mut fetch = goal.saturating_sub(out.len());
669    loop {
670        let mut scratch = Vec::new();
671        store.lookup_prefix(prefix, fetch, &mut scratch)?;
672        let scratch_len = scratch.len();
673        let before = out.len();
674        for oid in scratch {
675            if seen.insert(oid) {
676                out.push(oid);
677                if out.len() >= goal {
678                    return Ok(());
679                }
680            }
681        }
682        if out.len() >= goal {
683            return Ok(());
684        }
685        if scratch_len < fetch {
686            return Ok(());
687        }
688        let need = goal.saturating_sub(out.len());
689        if need == 0 {
690            return Ok(());
691        }
692        fetch = if before == out.len() {
693            fetch.saturating_add(need)
694        } else {
695            scratch_len.saturating_add(need)
696        };
697    }
698}