Skip to main content

coven_sync/
file_read.rs

1//! Authenticated, bounded ranged reads with cached ciphertext and read-ahead.
2
3use super::FilesMode;
4use crate::{
5    files::file_failure::stored,
6    files::file_status::Uploading,
7    files::{Files, FilesInner},
8    FileReadError,
9};
10use coven_crypto::{ContentHash, ContentHasher, FileKey};
11use coven_database::{
12    CacheReservation, DbError, FileLocation, FilePlace, FileRef, LocalFileStream, TransferEnd,
13    TransferMeter,
14};
15use coven_database::{FileLocationKey, Writer};
16use coven_format::{
17    chunks::CHUNK_SIZE,
18    file::{FileHeader, FILE_HEADER_LEN},
19};
20use coven_foundation::files::StoreDirAccess;
21use coven_foundation::files::{FileName, StoreReadLock};
22use coven_foundation::locks::read_unpoisoned;
23use coven_storage::{ByteProgress, ByteRange, ObjectPath};
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::Arc;
26
27use super::file_error::TransferError;
28
29#[path = "file_read_partial.rs"]
30mod partial;
31#[path = "file_read_parts.rs"]
32mod parts;
33#[path = "file_read_whole.rs"]
34mod whole;
35
36const REQUEST_BYTES: u64 = 1024 * 1024;
37/// An open file. A file never changes its bytes, so nothing later changes
38/// what it reads. Its shared store lock prevents deletion until the stream
39/// and its I/O finish, including reads still running after the store closes
40/// or the caller cancels.
41pub struct FileStream<M: FilesMode = Writer> {
42    id: String,
43    source: OpenFile<M>,
44    state: tokio::sync::Mutex<FileStreamState>,
45}
46enum OpenFile<M: FilesMode> {
47    Local {
48        stream: LocalFileStream,
49        owner: Arc<FilesInner<M>>,
50        file: FileRef,
51        /// Its upload, while one is queued: reads settle or record its
52        /// source's failure there, or on its move. A file with no upload
53        /// records nothing; its reads' errors go to the caller.
54        uploading: Option<Arc<Uploading>>,
55    },
56    Uploaded(Arc<UploadedFile<M>>),
57}
58struct FileStreamState {
59    end: Option<u64>,
60    hash: Option<ContentHasher>,
61    ahead: Option<coven_foundation::task::OwnedTask>,
62}
63impl Drop for FileStreamState {
64    fn drop(&mut self) {
65        if let Some(task) = &self.ahead {
66            task.abort();
67        }
68    }
69}
70pub(crate) struct UploadedFile<M: FilesMode = Writer> {
71    owner: Arc<FilesInner<M>>,
72    file: FileRef,
73    /// Where its bytes are stored.
74    location: FileLocation,
75    /// The SHA-256 of its bytes, which a whole read checks.
76    hash: ContentHash,
77    /// The layout the file's size fixes; the stored header must equal it
78    /// before any of its chunks is trusted.
79    header: FileHeader,
80    /// Whether this open has checked the stored header, from the cache or
81    /// from storage. Set under the file's fetch lock.
82    header_checked: std::sync::atomic::AtomicBool,
83    key: FileKey,
84    path: ObjectPath,
85    /// The parts of its object it holds, on a provider whose ranged read
86    /// moves whole parts; `None` on one whose range moves only its bytes.
87    /// Settled at its first fetch, from the provider's part size (§17.7).
88    parts: std::sync::OnceLock<Option<parts::HeldParts>>,
89    _lock: StoreReadLock,
90}
91/// Where a read that fetches from storage tells how much of its range is
92/// here: the count of the range's bytes in hand, cached or received, each
93/// time it grows while the read fetches. A read this device serves alone,
94/// from its own copy or the cache, tells it nothing (§17.7).
95#[derive(Clone)]
96pub struct ReadProgress {
97    report: Arc<dyn Fn(u64) + Send + Sync>,
98}
99impl std::fmt::Debug for ReadProgress {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("ReadProgress").finish_non_exhaustive()
102    }
103}
104impl ReadProgress {
105    /// Tell `report` each new count of the range's bytes in hand.
106    pub fn new(report: impl Fn(u64) + Send + Sync + 'static) -> Self {
107        Self { report: Arc::new(report) }
108    }
109    fn tell(&self, here: u64) {
110        (self.report)(here);
111    }
112}
113/// A read's range and how much of it the steps before this one put in hand,
114/// for the fetch that follows to count on from.
115struct RangeHere<'a> {
116    progress: &'a ReadProgress,
117    offset: u64,
118    len: u64,
119    here: u64,
120}
121enum ChunkDestination<'a> {
122    /// The cache, for a read; `None` for one whose progress nothing shows.
123    Cache(Option<RangeHere<'a>>),
124    /// A pin's whole file, whose download `progress` hears as its bytes
125    /// arrive; `fetched` counts the bytes this step's range received.
126    WholeFile { progress: &'a Arc<whole::WholeProgress>, fetched: &'a Arc<AtomicU64> },
127}
128impl<M: FilesMode> Files<M> {
129    /// Open the file as its record now stands: its bytes here while it is on
130    /// this device only, uploading or not, or else its cached or stored
131    /// bytes. A file only on another device can't be read here (§17.7).
132    pub async fn open_file_stream(&self, file: &FileRef) -> Result<FileStream<M>, FileReadError> {
133        self.inner.check_open()?;
134        let mut file = self.inner.current(file).await?;
135        if let FilePlace::DeviceOnly { .. } = file.place() {
136            let uploading = self.inner.local(&file).await?.map(Arc::new);
137            let local = self.inner.database.open_local(&file).await.map_err(FileReadError::from);
138            match local {
139                Ok(Some(stream)) => {
140                    return Ok(FileStream::new(
141                        &file,
142                        OpenFile::Local {
143                            stream,
144                            owner: self.inner.clone(),
145                            file: file.clone(),
146                            uploading,
147                        },
148                    ))
149                }
150                // Its upload completed meanwhile, even as it opened, and its
151                // bytes are stored; or its record went, or says another
152                // device has it.
153                Ok(None) => file = self.inner.current(&file).await?,
154                Err(error) => {
155                    return match &uploading {
156                        Some(uploading) => {
157                            self.inner.complete_local_read(file.id(), uploading, Err(error)).await
158                        }
159                        None => Err(error),
160                    }
161                }
162            }
163        }
164        if let FilePlace::DeviceOnly { device } = file.place() {
165            return Err(self.inner.unreadable(&file, device).await);
166        }
167        let opened = match UploadedFile::open(self.inner.clone(), file.clone()).await {
168            Ok(opened) => opened,
169            Err(error) => return self.inner.complete_read(&file, Err(error.into()), false).await,
170        };
171        Ok(FileStream::new(&file, OpenFile::Uploaded(Arc::new(opened))))
172    }
173    /// Read the whole file. A stored file is checked against its content
174    /// hash; a file this device uploads reads as it is on disk (§17.7).
175    pub async fn read_file(&self, file: &FileRef) -> Result<Vec<u8>, FileReadError> {
176        self.open_file_stream(file).await?.read_at(0, file.size()).await
177    }
178}
179impl<M: FilesMode> FileStream<M> {
180    fn new(file: &FileRef, source: OpenFile<M>) -> Self {
181        Self {
182            id: file.id().to_string(),
183            source,
184            state: tokio::sync::Mutex::new(FileStreamState {
185                end: Some(0),
186                hash: Some(ContentHasher::new()),
187                ahead: None,
188            }),
189        }
190    }
191
192    /// Complete plaintext size.
193    pub fn plaintext_size(&self) -> u64 {
194        match &self.source {
195            OpenFile::Local { stream, .. } => stream.plaintext_size(),
196            OpenFile::Uploaded(file) => file.header.size(),
197        }
198    }
199    /// Read exactly the requested range. Network requests cover only missing
200    /// chunks, batching adjacent chunks within 1 MiB. Use `read_range` to keep
201    /// returned plaintext bounded independently of the requested range length.
202    pub async fn read_at(&self, offset: u64, len: u64) -> Result<Vec<u8>, FileReadError> {
203        self.read(offset, len, None).await
204    }
205    /// [`Self::read_at`], telling `progress` how many of the range's bytes are
206    /// here each time that grows while the read fetches from storage. A read
207    /// served from this device's copy or the cache tells it nothing.
208    pub async fn read_at_reporting(
209        &self,
210        offset: u64,
211        len: u64,
212        progress: &ReadProgress,
213    ) -> Result<Vec<u8>, FileReadError> {
214        self.read(offset, len, Some(progress)).await
215    }
216    async fn read(
217        &self,
218        offset: u64,
219        len: u64,
220        progress: Option<&ReadProgress>,
221    ) -> Result<Vec<u8>, FileReadError> {
222        match &self.source {
223            OpenFile::Local { stream, owner, file, uploading } => {
224                let read = stream.read_at(offset, len).await.map_err(FileReadError::from);
225                match uploading {
226                    Some(uploading) => owner.complete_local_read(file.id(), uploading, read).await,
227                    None => read,
228                }
229            }
230            OpenFile::Uploaded(file) => {
231                file.check_range(offset, len)?;
232                let mut state = self.state.lock().await;
233                let sequential = state.end == Some(offset);
234                if !sequential {
235                    if let Some(task) = state.ahead.take() {
236                        task.abort();
237                    }
238                }
239                if offset == 0 {
240                    state.hash = Some(ContentHasher::new());
241                } else if !sequential {
242                    state.hash = None;
243                }
244                let read = file.range(offset, len, progress).await;
245                let bytes = match read {
246                    Ok(bytes) => bytes,
247                    Err(error) => {
248                        return file.owner.complete_read(&file.file, Err(error.into()), false).await
249                    }
250                };
251                let mut whole = false;
252                if let Some(hash) = &mut state.hash {
253                    hash.update(&bytes);
254                }
255                if offset + len == file.header.size() {
256                    if let Some(hash) = state.hash.take() {
257                        if hash.finish() != file.hash {
258                            return file
259                                .owner
260                                .complete_read(
261                                    &file.file,
262                                    Err(TransferError::Refused(crate::Refusal::ContentHash)),
263                                    false,
264                                )
265                                .await;
266                        }
267                        whole = true;
268                    }
269                }
270                state.end = Some(offset + len);
271                if sequential && offset > 0 && offset + len < file.header.size() {
272                    if let Some(task) = state.ahead.take() {
273                        task.abort();
274                    }
275                    let ahead = file.clone();
276                    let start = offset + len;
277                    // Nobody waits for it: a panic in it goes to the store's
278                    // bug sink, and the next read fetches what it would have.
279                    state.ahead = Some(coven_foundation::task::spawn_owned(
280                        "a read-ahead",
281                        file.owner.database.bugs().clone(),
282                        async move {
283                            let length = (ahead.header.size() - start).min(REQUEST_BYTES);
284                            let result =
285                                ahead.range(start, length, None).await.map_err(TransferError::from);
286                            ahead.owner.finish_read_ahead(&ahead.file, result).await;
287                        },
288                        |_| async {},
289                    ));
290                }
291                file.owner.complete_read(&file.file, Ok(bytes), whole).await
292            }
293        }
294    }
295    /// Stream a byte range in bounded plaintext buffers, authenticating each
296    /// fetched chunk before yielding its bytes. For a stored file, the final
297    /// buffer checks the whole-file hash when reading sequentially from zero
298    /// through the end.
299    pub fn read_range(
300        &self,
301        offset: u64,
302        len: u64,
303    ) -> Result<FileRangeStream<'_, M>, FileReadError> {
304        if offset.checked_add(len).is_none_or(|end| end > self.plaintext_size()) {
305            return Err(FileReadError::RangeOutOfBounds {
306                id: self.id.clone(),
307                offset,
308                end: offset.saturating_add(len),
309                size: self.plaintext_size(),
310            });
311        }
312        Ok(FileRangeStream { file: self, offset, remaining: len, empty: len == 0 })
313    }
314}
315/// A borrowed stream of bounded authenticated plaintext buffers.
316pub struct FileRangeStream<'a, M: FilesMode = Writer> {
317    file: &'a FileStream<M>,
318    offset: u64,
319    remaining: u64,
320    empty: bool,
321}
322impl<M: FilesMode> FileRangeStream<'_, M> {
323    /// Yield the next buffer, or `None` after the exact requested range.
324    pub async fn next(&mut self) -> Result<Option<Vec<u8>>, FileReadError> {
325        if self.empty {
326            self.empty = false;
327            self.file.read_at(self.offset, self.remaining).await?;
328            self.remaining = 0;
329            return Ok(None);
330        }
331        if self.remaining == 0 {
332            return Ok(None);
333        }
334        let length = self.remaining.min(REQUEST_BYTES - CHUNK_SIZE as u64);
335        let bytes = self.file.read_at(self.offset, length).await?;
336        self.offset += length;
337        self.remaining -= length;
338        Ok(Some(bytes))
339    }
340}
341impl FilesInner {
342    /// A pin's whole copy of the stored file, downloaded as `keep_whole`
343    /// assembles it (§17.8), or `None` when a metered network the app keeps
344    /// files off stopped it before a request to storage: nothing failed.
345    pub(crate) async fn download_whole(
346        self: &Arc<Self>,
347        file: FileRef,
348    ) -> Result<Option<CacheReservation>, TransferError> {
349        // An empty file's header is its whole object, which opening reads.
350        if file.size() == 0 && self.downloads_held() {
351            return Ok(None);
352        }
353        UploadedFile::open(self.clone(), file).await?.keep_whole().await
354    }
355}
356impl<M: FilesMode> UploadedFile<M> {
357    /// Open an uploaded file without a request when its header is cached.
358    /// Otherwise the first fetch checks the header: one starting at chunk
359    /// zero carries it in the same request, and any other first fetch reads
360    /// it alone (§17.7). An empty file's header is the whole object, so it
361    /// is checked here.
362    pub(crate) async fn open(
363        owner: Arc<FilesInner<M>>,
364        file: FileRef,
365    ) -> Result<Self, FileReadError> {
366        let (location, hash) = stored(&file)?;
367        let location = location.clone();
368        let directory = owner.directory.clone();
369        let lock = coven_foundation::task::blocking_completing(
370            "lock the store for reading",
371            owner.database.bugs().clone(),
372            move || directory.lock_read_only(),
373        )
374        .await
375        .map_err(coven_foundation::files::FileError::from)??;
376        let coven_format::file_reference::FileReference { device, object, key } =
377            location.reference()?;
378        let path = ObjectPath::file(owner.directory.id(), device, object);
379        let opened = Self {
380            header: FileHeader::new(file.size()),
381            header_checked: std::sync::atomic::AtomicBool::new(false),
382            owner,
383            file,
384            location,
385            hash,
386            key,
387            path,
388            parts: std::sync::OnceLock::new(),
389            _lock: lock,
390        };
391        if opened.header.chunk_count() == 0 {
392            let _file = opened.owner.lock_file(&opened.file, &opened.location).await;
393            let _guard = opened.owner.cache.read().await;
394            let cached = opened.cached_header().await?;
395            if cached != CachedHeader::Checked {
396                opened.fetch_header(cached).await?;
397            }
398        }
399        Ok(opened)
400    }
401    /// Check the header cached for this file, if one is.
402    async fn cached_header(&self) -> Result<CachedHeader, FileReadError> {
403        if self.header_checked.load(std::sync::atomic::Ordering::Acquire) {
404            return Ok(CachedHeader::Checked);
405        }
406        let Some(bytes) = self.owner.database.cached(&self.file, -1).await? else {
407            return Ok(CachedHeader::Missing);
408        };
409        // The cache keeps only a header that checked, so one that fails now
410        // changed on this device.
411        Ok(match self.accept_header(&bytes) {
412            Ok(()) => CachedHeader::Checked,
413            Err(_) => CachedHeader::Damaged,
414        })
415    }
416    /// Read the header alone and check it, then cache it, or write it back
417    /// over the cached copy that failed its check.
418    async fn fetch_header(&self, cached: CachedHeader) -> Result<(), FileReadError> {
419        let bytes = self.fetch(0, FILE_HEADER_LEN as u64, &ByteProgress::unread()).await?;
420        if let Err(refused) = self.accept_header(&bytes) {
421            return Err(self.confirm(0, &bytes, refused).await);
422        }
423        match cached {
424            CachedHeader::Damaged => {
425                Ok(self.owner.database.repair_cached(&self.file, -1, bytes).await?)
426            }
427            CachedHeader::Checked | CachedHeader::Missing => self.publish_header(bytes).await,
428        }
429    }
430    /// A refusal judges the stored file, not one fetch of it, and nothing
431    /// checks a fetch's transfer: bytes from `start` that a check refused are
432    /// fetched again, and the refusal stands only when the second fetch
433    /// returns the same bytes. Different bytes mean a fault on the way
434    /// changed a fetch, which fails as a read with `Corrupted` (§19.1).
435    async fn confirm(&self, start: u64, first: &[u8], refused: FileReadError) -> FileReadError {
436        if !matches!(refused, FileReadError::Integrity { .. }) {
437            return refused;
438        }
439        let end = start + first.len() as u64;
440        // The second fetch asks storage, never the parts this open file
441        // holds, which would hand back the same bytes (§17.7).
442        if let Some(Some(parts)) = self.parts.get() {
443            parts.forget(start, end);
444        }
445        match self.fetch(start, end, &ByteProgress::unread()).await {
446            Ok(again) if again == first => refused,
447            Ok(_) => coven_storage::StorageFailure::Corrupted
448                .with_source("a second fetch of the file returned other bytes")
449                .into(),
450            Err(error) => error,
451        }
452    }
453    /// Check fetched header bytes, then cache them before any chunk.
454    async fn publish_header(&self, bytes: Vec<u8>) -> Result<(), FileReadError> {
455        self.accept_header(&bytes)?;
456        cache(&self.owner, &self.file, -1, 1, bytes).await?;
457        self.owner.trim(self.file.namespace()).await;
458        Ok(())
459    }
460    /// The stored header must be the one the row's size fixes; a newer format
461    /// needs an update, anything else is damage at the fixed path.
462    fn accept_header(&self, bytes: &[u8]) -> Result<(), FileReadError> {
463        let header = FileHeader::decode(bytes).map_err(|error| match error {
464            coven_format::Error::UnsupportedVersion(version)
465                if version > coven_format::FORMAT_VERSION =>
466            {
467                FileReadError::UpdateRequired(crate::RequiredUpdate::CovenFormat { version })
468            }
469            _ => self.integrity(),
470        })?;
471        if header != self.header {
472            return Err(self.integrity());
473        }
474        self.header_checked.store(true, std::sync::atomic::Ordering::Release);
475        Ok(())
476    }
477    fn integrity(&self) -> FileReadError {
478        FileReadError::Integrity { id: self.file.id().to_string(), cause: None }
479    }
480    fn check_range(&self, offset: u64, len: u64) -> Result<(), FileReadError> {
481        if offset.checked_add(len).is_none_or(|end| end > self.header.size()) {
482            Err(FileReadError::RangeOutOfBounds {
483                id: self.file.id().to_string(),
484                offset,
485                end: offset.saturating_add(len),
486                size: self.header.size(),
487            })
488        } else {
489            Ok(())
490        }
491    }
492    #[expect(
493        clippy::map_err_ignore,
494        reason = "a size past the target integer is TooLarge, which names the field and size"
495    )]
496    pub(crate) async fn range(
497        &self,
498        offset: u64,
499        len: u64,
500        progress: Option<&ReadProgress>,
501    ) -> Result<Vec<u8>, FileReadError> {
502        self.owner.check_open()?;
503        self.check_range(offset, len)?;
504        let length = usize::try_from(len).map_err(|_| DbError::TooLarge {
505            field: "file range",
506            actual: len,
507            maximum: usize::MAX as u64,
508        })?;
509        let mut result = Vec::new();
510        result.try_reserve_exact(length).map_err(|_| DbError::TooLarge {
511            field: "file range",
512            actual: len,
513            maximum: usize::MAX as u64,
514        })?;
515        if len == 0 {
516            return Ok(result);
517        }
518        let size = CHUNK_SIZE as u64;
519        let mut index = offset / size;
520        let last = (offset + len - 1) / size;
521        while index <= last {
522            let here = result.len() as u64;
523            let destination = ChunkDestination::Cache(progress.map(|progress| RangeHere {
524                progress,
525                offset,
526                len,
527                here,
528            }));
529            let ChunkStep::Read(end, chunks) = self.chunks(index, last, destination).await? else {
530                unreachable!("only a pin's download waits for the network")
531            };
532            for (number, chunk) in chunks.into_iter().enumerate() {
533                let plain = chunk.plaintext;
534                let position = (index + number as u64) * size;
535                let start = offset.saturating_sub(position) as usize;
536                let end = (offset + len - position).min(plain.len() as u64) as usize;
537                result.extend_from_slice(&plain[start..end]);
538            }
539            index = end + 1;
540        }
541        Ok(result)
542    }
543    // Reads of one file coalesce; independent files can fetch concurrently,
544    // and so can a pin's ranges of one file, which put nothing in the cache.
545    // The shared cache guard excludes eviction and store close during a fetch.
546    #[expect(
547        clippy::map_err_ignore,
548        reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
549    )]
550    async fn chunks(
551        &self,
552        first: u64,
553        last: u64,
554        destination: ChunkDestination<'_>,
555    ) -> Result<ChunkStep, FileReadError> {
556        self.owner.check_open()?;
557        let file_lock = self.owner.lock_file(&self.file, &self.location).await;
558        let _guard = self.owner.cache.read().await;
559        // A cached copy that fails its check changed on this device, which
560        // says nothing about the stored file (§17.8): its bytes are fetched.
561        let cached = self
562            .owner
563            .database
564            .cached(&self.file, first as i64)
565            .await?
566            .map(|bytes| self.open_chunk(first, bytes));
567        let header = self.cached_header().await?;
568        // A pin's download asks storage for nothing while a metered network
569        // holds it; what is here already it still takes (§17.8).
570        if matches!(destination, ChunkDestination::WholeFile { .. })
571            && (!matches!(cached, Some(Ok(_))) || header != CachedHeader::Checked)
572            && self.owner.downloads_held()
573        {
574            return Ok(ChunkStep::Held);
575        }
576        // An unchecked header comes with an uncached chunk zero; any other
577        // first read checks it alone (§17.7).
578        let mut with_header = false;
579        if header == CachedHeader::Missing && first == 0 && cached.is_none() {
580            with_header = true;
581        } else if header != CachedHeader::Checked {
582            self.fetch_header(header).await?;
583        }
584        match cached {
585            Some(Ok(chunk)) => return Ok(ChunkStep::Read(first, vec![chunk])),
586            Some(Err(_)) => {
587                return Ok(ChunkStep::Read(
588                    first,
589                    vec![self.write_back(first, &destination).await?],
590                ))
591            }
592            None => {}
593        }
594        let first_chunk = self.header.chunk(first).map_err(|_| self.integrity())?;
595        let start = first_chunk.offset;
596        let mut end_index = first;
597        // The fetch stops at the first cached chunk after the ones it needs.
598        // That chunk's bytes, read here, finish this step, so it is read once
599        // and counts as read before the trim that follows caching the fetched
600        // range.
601        let mut stop = None;
602        let mut end = first_chunk.end();
603        while end_index < last {
604            let next = self.header.chunk(end_index + 1).map_err(|_| self.integrity())?;
605            let next_end = next.end();
606            let request_start = if with_header { 0 } else { start };
607            if next_end - request_start > REQUEST_BYTES {
608                break;
609            }
610            if let Some(bytes) =
611                self.owner.database.cached(&self.file, (end_index + 1) as i64).await?
612            {
613                stop = Some(bytes);
614                break;
615            }
616            end_index += 1;
617            end = next_end;
618        }
619        let received = self.received(&destination, first, end_index, start)?;
620        let request_start = if with_header { 0 } else { start };
621        // A pin's fetch puts nothing in the cache, so its file's other reads
622        // need not wait for it: the lock covered reading the cache and
623        // checking the header. A header that comes with chunk zero is
624        // published under it, and the reads after wait to find it checked.
625        if matches!(destination, ChunkDestination::WholeFile { .. }) && !with_header {
626            drop(file_lock);
627        }
628        let fetched = self.fetch(request_start, end, &received).await?;
629        // The header precedes chunk zero: one request carries both, and the
630        // header is cached once it checks, whatever the chunks turn out.
631        let chunks_start = if with_header {
632            let header = fetched.get(..FILE_HEADER_LEN).ok_or_else(|| self.integrity())?;
633            if let Err(refused) = self.accept_header(header) {
634                return Err(self.confirm(request_start, &fetched, refused).await);
635            }
636            self.publish_header(header.to_vec()).await?;
637            FILE_HEADER_LEN
638        } else {
639            0
640        };
641        let (bytes, mut plain) = match self.open_fetched(&fetched[chunks_start..], first, end_index)
642        {
643            Ok(opened) => opened,
644            Err(refused) => return Err(self.confirm(request_start, &fetched, refused).await),
645        };
646        // Every chunk of the range authenticated: it is published as one
647        // file and one row. A failed chunk publishes none of it. A pin writes
648        // these bytes to its reserved whole file instead; adding them to the
649        // cache too could evict a cached tail before pinning reaches it.
650        if matches!(destination, ChunkDestination::Cache(_)) {
651            cache(&self.owner, &self.file, first as i64, (end_index - first + 1) as i64, bytes)
652                .await?;
653            self.owner.trim(self.file.namespace()).await;
654        }
655        // A cached chunk that fails its check is left for the next step,
656        // which fetches it as its first.
657        if let Some(Ok(chunk)) = stop.map(|bytes| self.open_chunk(end_index + 1, bytes)) {
658            end_index += 1;
659            plain.push(chunk);
660        }
661        Ok(ChunkStep::Read(end_index, plain))
662    }
663    /// Chunk `index`'s cached copy failed its check: damage on this device,
664    /// which says nothing about the stored file (§17.8). Its stored bytes are
665    /// fetched, judged as any fetch's are, and written back over the copy.
666    #[expect(
667        clippy::map_err_ignore,
668        reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
669    )]
670    async fn write_back(
671        &self,
672        index: u64,
673        destination: &ChunkDestination<'_>,
674    ) -> Result<OpenedChunk, FileReadError> {
675        let chunk = self.header.chunk(index).map_err(|_| self.integrity())?;
676        let received = self.received(destination, index, index, chunk.offset)?;
677        let fetched = self.fetch(chunk.offset, chunk.end(), &received).await?;
678        let opened = match self.open_chunk(index, fetched.clone()) {
679            Ok(opened) => opened,
680            Err(refused) => return Err(self.confirm(chunk.offset, &fetched, refused).await),
681        };
682        self.owner.database.repair_cached(&self.file, index as i64, fetched).await?;
683        Ok(opened)
684    }
685    /// What hears a fetch of chunks `first..=last`, which starts `start`
686    /// bytes into the object. A pin's meter hears the furthest through the
687    /// object the bytes received reach. A read that shows its progress hears,
688    /// from the fetch's start, its range's bytes in hand: those the steps
689    /// before had, and the received part of each fetched chunk that falls in
690    /// the range.
691    #[expect(
692        clippy::map_err_ignore,
693        reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
694    )]
695    fn received(
696        &self,
697        destination: &ChunkDestination<'_>,
698        first: u64,
699        last: u64,
700        start: u64,
701    ) -> Result<ByteProgress, FileReadError> {
702        match destination {
703            ChunkDestination::WholeFile { progress, fetched } => {
704                let (progress, fetched) = ((*progress).clone(), (*fetched).clone());
705                let furthest = AtomicU64::new(start);
706                Ok(ByteProgress::new(move |reached| {
707                    let before = furthest.fetch_max(reached, Ordering::Relaxed);
708                    if reached > before {
709                        fetched.fetch_add(reached - before, Ordering::Relaxed);
710                        progress.arrived(reached - before);
711                    }
712                }))
713            }
714            ChunkDestination::Cache(None) => Ok(ByteProgress::unread()),
715            ChunkDestination::Cache(Some(range)) => {
716                // Each fetched chunk's place in the object, its plaintext
717                // length, and the part of the range it holds.
718                let mut chunks = Vec::new();
719                for index in first..=last {
720                    let chunk = self.header.chunk(index).map_err(|_| self.integrity())?;
721                    let plain_start = chunk.plaintext_start();
722                    let from = range.offset.max(plain_start) - plain_start;
723                    let to = (range.offset + range.len)
724                        .min(plain_start + chunk.plaintext_length as u64)
725                        - plain_start;
726                    chunks.push((chunk.offset, chunk.plaintext_length as u64, from, to));
727                }
728                let progress = range.progress.clone();
729                let here = range.here;
730                progress.tell(here);
731                Ok(ByteProgress::new(move |reached| {
732                    let fetched: u64 = chunks
733                        .iter()
734                        .map(|&(offset, length, from, to)| {
735                            let received = reached.saturating_sub(offset).min(length);
736                            received.clamp(from, to) - from
737                        })
738                        .sum();
739                    progress.tell(here + fetched);
740                }))
741            }
742        }
743    }
744    /// Open fetched chunks `first..=last`, every one of which must
745    /// authenticate: their sealed bytes, and each one opened.
746    #[expect(
747        clippy::map_err_ignore,
748        reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
749    )]
750    fn open_fetched(
751        &self,
752        fetched: &[u8],
753        first: u64,
754        last: u64,
755    ) -> Result<(Vec<u8>, Vec<OpenedChunk>), FileReadError> {
756        let bytes = fetched.to_vec();
757        let mut plain = Vec::new();
758        let mut cursor = 0;
759        for index in first..=last {
760            let length = self.header.chunk(index).map_err(|_| self.integrity())?.stored_length();
761            let sealed = bytes.get(cursor..cursor + length).ok_or_else(|| self.integrity())?;
762            let opened = self
763                .header
764                .open_chunk(&self.key, self.path.as_str(), index, sealed)
765                .map_err(|error| FileReadError::chunk(self.file.id().to_string(), error))?;
766            plain.push(OpenedChunk { plaintext: opened, ciphertext: sealed.to_vec() });
767            cursor += length;
768        }
769        Ok((bytes, plain))
770    }
771    /// Authenticate chunk `index`'s sealed bytes.
772    fn open_chunk(&self, index: u64, bytes: Vec<u8>) -> Result<OpenedChunk, FileReadError> {
773        let plaintext = self
774            .header
775            .open_chunk(&self.key, self.path.as_str(), index, &bytes)
776            .map_err(|error| FileReadError::chunk(self.file.id().to_string(), error))?;
777        Ok(OpenedChunk { plaintext, ciphertext: bytes })
778    }
779}
780
781struct OpenedChunk {
782    plaintext: Vec<u8>,
783    ciphertext: Vec<u8>,
784}
785
786/// What the cache holds of a file's header.
787#[derive(Clone, Copy, PartialEq, Eq)]
788enum CachedHeader {
789    /// A header this open checked, from the cache or from storage.
790    Checked,
791    /// No header.
792    Missing,
793    /// A copy that fails its check, which changed on this device (§17.8).
794    Damaged,
795}
796
797/// What one step of reading chunks found.
798enum ChunkStep {
799    /// The chunks from the first asked for through this one.
800    Read(u64, Vec<OpenedChunk>),
801    /// A pin's download needs storage, and a metered network holds it.
802    Held,
803}
804
805async fn cache<M: FilesMode>(
806    owner: &FilesInner<M>,
807    file: &FileRef,
808    first: i64,
809    chunks: i64,
810    bytes: Vec<u8>,
811) -> Result<(), FileReadError> {
812    let name = FileName::new(owner.ids.new_id().to_string()).expect("UUID cache name");
813    owner.database.cache_range(file, first, chunks, name, bytes).await?;
814    Ok(())
815}
816impl<M: FilesMode> UploadedFile<M> {
817    /// Fetch `start..end` of the file's object, telling `received` how far
818    /// through the object the bytes received reach. On a provider whose
819    /// ranged read moves whole parts, the bytes come from the parts this open
820    /// file holds, each fetched once (§17.7).
821    #[expect(
822        clippy::map_err_ignore,
823        reason = "a size whose encrypted layout overflows is the disagreement Integrity names"
824    )]
825    async fn fetch(
826        &self,
827        start: u64,
828        end: u64,
829        received: &ByteProgress,
830    ) -> Result<Vec<u8>, FileReadError> {
831        let storage =
832            read_unpoisoned(&self.owner.storage).clone().ok_or(FileReadError::NoStorage)?;
833        let size = self.header.encrypted_size().map_err(|_| self.integrity())?;
834        let parts = self
835            .parts
836            .get_or_init(|| storage.read_part_size().map(|part| parts::HeldParts::new(part, size)));
837        let (owner, file, path) = (&self.owner, &self.file, &self.path);
838        match parts {
839            None => fetch(owner, file, path, storage.as_ref(), start, end, received).await,
840            Some(parts) => {
841                parts
842                    .read(start, end, received, |from, to, heard| {
843                        let storage = storage.clone();
844                        async move {
845                            fetch(owner, file, path, storage.as_ref(), from, to, &heard).await
846                        }
847                    })
848                    .await
849            }
850        }
851    }
852}
853
854/// Fetch `start..end` of the file's object from `storage`, telling
855/// `received` how far through the object the bytes received reach: storage
856/// tells it as the stream is read, and CloudKit also as each part's fetch
857/// goes, ahead of the read.
858async fn fetch<M: FilesMode>(
859    owner: &FilesInner<M>,
860    file: &FileRef,
861    path: &ObjectPath,
862    storage: &dyn coven_storage::Storage,
863    start: u64,
864    end: u64,
865    received: &ByteProgress,
866) -> Result<Vec<u8>, FileReadError> {
867    // A stored file's record names it only once it is stored (§17.2):
868    // confirmed absence means storage lost it, never a provider failure or
869    // an upload to wait for.
870    let mut stream = match storage
871        .stream_object_reporting(path, Some(ByteRange::new(start, end)?), received)
872        .await
873    {
874        Ok(stream) => stream,
875        Err(error) if error.failure() == coven_storage::StorageFailure::NotFound => {
876            let (location, _) = stored(file)?;
877            return Err(owner.absence_error(file, location));
878        }
879        // Every requested range lies within the file's encrypted size; an
880        // object too short to hold it is damage at the fixed path.
881        Err(error) if error.failure() == coven_storage::StorageFailure::InvalidRange => {
882            return Err(FileReadError::Integrity { id: file.id().to_string(), cause: None })
883        }
884        Err(error) => return Err(error.into()),
885    };
886    // The reply describes the complete object (§20.5). Any size but the file's is
887    // damage, as file status says, even when the requested range is there.
888    crate::files::file_status::complete(file, stream.metadata())?;
889    let mut bytes = Vec::with_capacity((end - start) as usize);
890    let mut buffer = vec![0; 64 * 1024];
891    loop {
892        let length = stream.read(&mut buffer).await?;
893        if length == 0 {
894            break;
895        }
896        bytes.extend_from_slice(&buffer[..length]);
897    }
898    if bytes.len() as u64 != end - start {
899        return Err(FileReadError::Integrity { id: file.id().to_string(), cause: None });
900    }
901    Ok(bytes)
902}