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

//!
//! Tools to delete object(s) in a MOSAIC file
//!

use crate::backends::MosaicBackend;
use crate::creator::BufferedMasterElement;
use crate::ebml::{MosaicTag, CRC32_SIZE};
use crate::reader::MosaicReader;
use crate::writer::MosaicWriter;
use crate::IdxDescription;
use crate::{Position, Size};
use anyhow::Result;
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
use std::io::{Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use thiserror::Error;

pub struct MosaicUpdater<B: MosaicBackend> {
    path: PathBuf,
    reader: MosaicReader<B>,
    writer: MosaicWriter,
}

/// Typed errors for MosaicUpdater
#[derive(Error, Debug)]
pub enum MosaicUpdaterError {
    #[error("Key {key:?} not found")]
    KeyNotFound { key: Vec<u8> },

    #[error("Missing key of index {idx_description}")]
    MissingKey { idx_description: IdxDescription },

    #[error("{key_1:?} and {key_2:?} should refer to the same object but don't")]
    MismatchingKeys { key_1: Vec<u8>, key_2: Vec<u8> },

    #[error("Object with key {key:?} for index {idx_description} not found")]
    ObjectNotFound {
        key: Vec<u8>,
        idx_description: IdxDescription,
    },
}

impl<B: MosaicBackend> MosaicUpdater<B> {
    pub fn new(path: &Path, backend: B) -> Result<Self> {
        let reader = MosaicReader::with_backend(path, Arc::new(backend))?;
        let writer = MosaicWriter::new(File::options().read(true).write(true).open(path)?)?;

        let updater = MosaicUpdater {
            path: path.to_path_buf(),
            reader,
            writer,
        };

        Ok(updater)
    }

    /// Update the object counter to `val`
    fn update_object_counter(&mut self, val: u64) -> Result<()> {
        let (_, mosaic_offset) = self
            .reader
            .backend
            .find_element(0.into(), MosaicTag::Mosaic)?;

        let (_, md_offset) = self
            .reader
            .backend
            .find_element(mosaic_offset, MosaicTag::ContainerMetaData)?;

        self.writer
            .file
            .seek(SeekFrom::Start((md_offset + CRC32_SIZE).0.try_into()?))?;

        let mut buffered_master = BufferedMasterElement::new();
        buffered_master.append_full_uint(MosaicTag::ObjectsCounter, val)?;

        self.writer.file.write_all(&buffered_master.buffer)?;
        self.writer.rewrite_crc32(mosaic_offset)?;
        Ok(())
    }

    /// Update the object counter to `val`
    fn update_object_total_size(&mut self, val: Size) -> Result<()> {
        let (_, mosaic_offset) = self
            .reader
            .backend
            .find_element(0.into(), MosaicTag::Mosaic)?;

        let (_, md_offset) = self
            .reader
            .backend
            .find_element(mosaic_offset, MosaicTag::ContainerMetaData)?;

        let (oc_size, oc_offset) = self
            .reader
            .backend
            .find_element(md_offset, MosaicTag::ObjectsCounter)?;
        let (_, ots_offset) = self.reader.backend.read_uint(oc_offset, oc_size)?;

        self.writer
            .file
            .seek(SeekFrom::Start((ots_offset).0.try_into()?))?;

        let mut buffered_master = BufferedMasterElement::new();
        buffered_master.append_full_uint(MosaicTag::ObjectsTotalSize, val.0)?;

        self.writer.file.write_all(&buffered_master.buffer)?;
        self.writer.rewrite_crc32(mosaic_offset)?;
        Ok(())
    }

    /// Replace the element at `offset` by a `MosaicTag::Void`, effectively deleting it.
    ///
    /// **Only works with elements whose tag is 1 byte long**, which is the case for
    /// `MosaicTag::Object`, `MosaicTag::Key` and `MosaicTag::Offset`.
    fn write_void(&mut self, offset: Position) -> Result<Size> {
        let (_, item_size, _) = self.reader.backend.find_tag_and_size(offset)?;
        self.writer
            .file
            .seek(SeekFrom::Start(offset.0.try_into()?))?;
        self.writer
            .write_master(MosaicTag::Void, &vec![0; item_size.0 as usize], false, None)?;

        Ok(item_size)
    }

    /// Deletes an object.
    ///
    /// It is done by replacing it, as well as all of its keys and offsets (across all indexes) by
    /// `MosaicTag::Void` elements.
    /// `MosaicTag::ObjectsCounter` and `MosaicTag::ObjectsTotalSize` are also updated to reflect
    /// the deletion.
    /// All impacted `MosaicTag::Crc32` elements are also updated.
    ///
    /// `object_indexes` should at least cover indexes contained in the updated file. This will return `MosaicUpdaterError::MissingKey` if a key is missing.
    pub fn delete(&mut self, object_indexes: Vec<(IdxDescription, &[u8])>) -> Result<()> {
        let indexes_list = self.reader.list_indexes()?;
        let file_indexes: Vec<_> = indexes_list.iter().map(|x| x.0).collect();
        let mut object_position = None;
        let mut object_key: Option<Vec<u8>> = None;
        let mut items_to_void = HashSet::new();
        let mut object_real_size = None;
        let mut crc32_to_update: HashSet<_> = indexes_list.iter().map(|x| x.1).collect();

        for idx in &file_indexes {
            if !object_indexes.iter().any(|x| x.0 == *idx) {
                return Err(MosaicUpdaterError::MissingKey {
                    idx_description: *idx,
                }
                .into());
            }
        }

        for (idx_description, key) in object_indexes.into_iter() {
            if !file_indexes.contains(&idx_description) {
                println!(
                    "Index {} not found in file {}, skipping it",
                    idx_description,
                    self.path.display()
                );
                continue;
            };

            self.reader.load_index(idx_description)?;

            let Some((key_offset, key_start_offset)) = self.reader.find_key(key)? else {
                return Err(MosaicUpdaterError::KeyNotFound { key: key.to_vec() }.into());
            };
            items_to_void.insert(key_start_offset);

            let key_size = Size(key.len().try_into()?);
            let (_, offset_start_offset) = self.reader.backend.read_binary(key_offset, key_size)?;
            items_to_void.insert(offset_start_offset);

            let Some((object_size, object_offset, object_start_offset)) =
                self.reader.find_object_offset(key_size, key_offset)?
            else {
                return Err(MosaicUpdaterError::ObjectNotFound {
                    key: key.to_vec(),
                    idx_description,
                }
                .into());
            };
            items_to_void.insert(object_start_offset);
            crc32_to_update.insert(self.reader.find_object_tile(object_offset)?);

            if object_real_size.is_none() {
                let obj = self.reader.read_object(object_offset, object_size)?;
                object_real_size = Some(Size(obj.len().try_into()?));
            };

            if let (Some(position), Some(obj_key)) = (object_position, &object_key) {
                if position != object_offset {
                    return Err(MosaicUpdaterError::MismatchingKeys {
                        key_1: obj_key.to_vec(),
                        key_2: key.to_vec(),
                    }
                    .into());
                }
            } else {
                object_position = Some(object_offset);
                object_key = Some(key.to_vec());
            }
        }

        for pos in items_to_void {
            self.write_void(pos)?;
        }

        for pos in crc32_to_update {
            self.writer.rewrite_crc32(pos)?;
        }

        // Update objects counter to reflect the object deletion
        self.update_object_counter(self.reader.objects_counter - 1)?;

        // Update objects total size to reflect the object deletion
        self.update_object_total_size(self.reader.objects_total_size - object_real_size.unwrap())?;

        Ok(())
    }
}