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

//! implementation of the `swh-mosaic check` subcommand

use std::path::Path;
use std::sync::Arc;

use anyhow::{bail, Result};

use crate::backends::{MmapMosaicBackend, MosaicBackend};
use crate::ebml::MosaicTag;
use crate::reader::{Cursor, MosaicReader, MosaicReaderError};

/// Check the integrity of a MOSAIC file
pub fn check(filename: &Path) -> Result<String> {
    let backend = Arc::new(MmapMosaicBackend::new(filename)?);
    // reader already checks EBML header and ContainerMetaData
    let reader = MosaicReader::with_backend(filename, backend.clone())?;
    let mut cursor = Cursor::new(backend.clone());

    // check root element's size
    let size = cursor.seek_to_element(MosaicTag::Mosaic)?;
    if backend.len() != cursor.offset + size {
        bail!(
            "Root element's length ({} from position {}) does not match file's size ({})",
            size,
            cursor.offset,
            backend.len()
        )
    }

    // skip ContainerMetadata (already checked when constructing `reader`)
    let md_size = cursor.seek_to_element(MosaicTag::ContainerMetaData)?;
    cursor.skip(md_size)?;

    // check tiles
    let mut nb_tiles = 0;
    let mut broken_tiles = 0;

    while cursor.offset < reader.end_of_tiles_offset {
        let next_tile_size = cursor.seek_to_and_check(MosaicTag::Tile);
        if let Ok(size) = next_tile_size {
            nb_tiles += 1;
            cursor.skip(size)?;
        } else {
            broken_tiles += 1;
            let broken_tile_size = cursor.seek_to_element(MosaicTag::Tile)?;
            cursor.skip(broken_tile_size)?;
        }
    }

    cursor.offset = reader.end_of_tiles_offset;

    // check indexes
    let mut nb_indexes = 0;
    let mut broken_indexes = 0;

    while cursor.offset < backend.len() {
        let next_index_size = cursor.seek_to_and_check(MosaicTag::Index);
        if let Ok(size) = next_index_size {
            nb_indexes += 1;
            cursor.skip(size)?;
        } else {
            broken_indexes += 1;
            let broken_index_size = cursor.seek_to_element(MosaicTag::Index)?;
            cursor.skip(broken_index_size)?;
        }
    }

    if broken_tiles == 0 && broken_indexes == 0 {
        Ok(format!(
            "{}: OK, {} tiles and {} indexes checked",
            filename.display(),
            nb_tiles,
            nb_indexes
        ))
    } else {
        Err(MosaicReaderError::BrokenMosaic {
            filename: filename.display().to_string(),
            broken_tiles,
            broken_indexes,
        }
        .into())
    }
}