swh-mosaic 0.3.1

MOdular Storage of Archived and Indexed Contents from Software Heritage
Documentation
// Copyright (C) 2026  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

//! This module contains implementations of `MosaicBackend`

use anyhow::Result;

pub mod mmap;

pub use mmap::MmapMosaicBackend;

use crate::{
    ebml::{crc32, MosaicTag, CRC32_SIZE},
    reader::MosaicReaderError,
    Position, Size,
};

/// Trait providing low-level operations for reading MOSAIC files
///
/// Implementers should provide `read_*` methods, users will prefer `find_*` methods.
pub trait MosaicBackend: Send + Sync {
    /// Reads an EBML tag and its size from given offset
    /// Returns the tag, element's size, and offset of the element's content
    /// This function skips voids automatically.
    fn find_tag_and_size(&self, mut offset: Position) -> Result<(MosaicTag, Size, Position)> {
        loop {
            let (tag, size, element_start) = self.read_one_tag_and_size(offset)?;

            if tag == MosaicTag::Void {
                offset = element_start + size;
            } else {
                return Ok((tag, size, element_start));
            }
        }
    }

    /// Lower-level, single tag implementation of find_tag_and_size. This one must
    /// be implemented by back-ends.
    fn read_one_tag_and_size(&self, offset: Position) -> Result<(MosaicTag, Size, Position)>;

    /// Finds the next element after `offset` in the MOSAIC whose tag matches the `element_tag`.
    ///
    /// Returns:
    ///  - the position of element's tag
    ///  - the size of the element's data
    ///  - the position of the element's data
    ///
    /// It will raise `MosaicReaderError::ElementNotFound(...)` if no element matches `element_tag`
    /// after `offset`.
    fn find_element_pre(
        &self,
        offset: Position,
        element_tag: MosaicTag,
    ) -> Result<(Position, Size, Position)> {
        if offset >= self.len() {
            return Err(MosaicReaderError::ElementNotFound { tag: element_tag }.into());
        }

        let mut tag_offset = offset;
        let (mut tag, mut size, mut offset) = self.find_tag_and_size(offset)?;
        let mut element_size = size;
        let mut element_offset = offset;

        loop {
            if tag != element_tag {
                offset += size;
                if offset >= self.len() {
                    return Err(MosaicReaderError::ElementNotFound { tag: element_tag }.into());
                }
                tag_offset = offset;
                (tag, size, offset) = self.find_tag_and_size(offset)?;
                element_size = size;
                element_offset = offset;
            } else {
                return Ok((tag_offset, element_size, element_offset));
            }
        }
    }

    /// Finds the next element after `offset` in the MOSAIC whose tag matches the `element_tag`.
    ///
    /// Returns the size of the element's data and the first offset of element's data.
    ///
    /// It will raise `MosaicReaderError::ElementNotFound(...)` if no element matches `element_tag`
    /// after `offset`.
    ///
    /// **Note** our algorithm finds sibling elements and moves one level up when
    /// reaching the end of a master element. But it does not dive into master elements.
    /// For example, in a file structured as:
    ///
    /// ```text
    /// A1
    /// ├─ B1
    /// |  ├─ C1
    /// |  └─ C2
    /// └─ B2
    ///    ├─ C3
    ///    └─ C4
    /// A2
    /// └─ B3
    /// ```
    ///
    /// If cursor is positioned at the beginning of B1, `find_element` will visit:
    ///  * C1
    ///  * C2
    ///  * B2
    ///  * A2
    ///
    fn find_element(&self, offset: Position, element_tag: MosaicTag) -> Result<(Size, Position)> {
        let (_, element_size, element_offset) = self.find_element_pre(offset, element_tag)?;
        Ok((element_size, element_offset))
    }

    /// Finds the next master element after `offset` in the MOSAIC whose tag matches the
    /// `master_tag`.
    ///
    /// It will also compute and validate the CRC32 of the found master element, and raise
    /// `MosaicReaderError::BadChecksum(...)` if the validation fails or
    /// `MosaicReaderError::MissingChecksum(...)` if no CRC32 element is found.
    ///
    /// Returns the size of the element's data and the first offset of element's data, adjusted to
    /// skip the CRC32 checksum.
    ///
    /// It will raise `MosaicReaderError::ElementNotFound(...)` if no element matches `master_tag`
    /// after `offset`.
    fn find_and_check(&self, offset: Position, master_tag: MosaicTag) -> Result<(Size, Position)> {
        if !master_tag.is_master() {
            panic!("{master_tag} is not a master element, try using MosaicReader::find_element() instead")
        }

        let (mut size, mut offset) = self.find_element(offset, master_tag)?;

        let (crc32_tag, crc32_size, next_offset) = self.find_tag_and_size(offset)?;
        if crc32_tag != MosaicTag::Crc32 {
            return Err(MosaicReaderError::MissingChecksum { tag: master_tag }.into());
        };
        size -= CRC32_SIZE;

        let (crc32_raw, next_offset) = self.read_binary(next_offset, crc32_size)?;
        let crc32_actual = crc32_raw.to_vec();
        let crc32_data = &self.read_binary(next_offset, size)?.0;
        let crc32_expected = crc32(crc32_data);
        if crc32_actual != crc32_expected {
            return Err(MosaicReaderError::BadChecksum { tag: master_tag }.into());
        };

        offset = next_offset;

        Ok((size, offset))
    }

    /// Reads binary data of given size from given offset
    /// Returns the binary data and next element's offset
    fn read_binary(&self, offset: Position, size: Size) -> Result<(&[u8], Position)>;

    /// Reads a single byte from given offset
    /// Returns the byte and next element's offset
    fn read_u8(&self, offset: Position) -> Result<(u8, Position)>;

    /// Reads an unsigned integer of given size from given offset
    /// Returns the integer and next element's offset
    fn read_uint(&self, offset: Position, size: Size) -> Result<(u64, Position)>;

    /// Reads a UTF-8 string of given size from given offset
    /// Returns the string and next element's offset
    fn read_utf8(&self, offset: Position, size: Size) -> Result<(String, Position)>;

    /// Total length of the back-end file
    fn len(&self) -> usize;

    /// The customary companion of len() (cf. clippy's len_without_is_empty)
    fn is_empty(&self) -> bool;
}