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 create` subcommand

use std::fs;
use std::path::{Path, PathBuf};

use anyhow::Result;
use blake2::{Blake2s256, Digest};
use bytes::Bytes;
use sha1::Sha1;
use sha2::Sha256;
use walkdir::WalkDir;

use crate::creator::MosaicCreator;
use crate::IdxDescription;

/// Create a MOSAIC file from a folder's contents (recursively)
pub fn create(
    input_dir: &Path,
    filename: &Path,
    tile_threshold: usize,
    comment: &[String],
    comment_file: &[PathBuf],
    idx_descriptions: &[IdxDescription],
    compression_level: Option<u8>,
) -> Result<()> {
    let mut comments = comment.to_vec();
    for path in comment_file {
        comments.push(fs::read_to_string(path)?);
    }
    let mut creator = MosaicCreator::new(
        filename,
        tile_threshold,
        comments,
        idx_descriptions.to_vec(),
        compression_level,
    )?;

    for entry in WalkDir::new(input_dir).into_iter().filter_map(|e| e.ok()) {
        if entry.file_type().is_file() {
            let path = entry.path();
            match fs::read(path) {
                Ok(content) => {
                    if let Err(e) =
                        creator.add(build_keys(&content, idx_descriptions), Bytes::from(content))
                    {
                        eprintln!("Error processing file {}: {}", path.display(), e);
                    } else {
                        println!("{}", path.display());
                    }
                }
                Err(e) => {
                    eprintln!("Error reading file {}: {}", path.display(), e);
                }
            }
        }
    }

    creator.close()?;

    Ok(())
}

pub fn build_keys(content: &[u8], idx_descriptions: &[IdxDescription]) -> Vec<Bytes> {
    idx_descriptions
        .iter()
        .map(|idx_description| match idx_description {
            IdxDescription::Sha1Fmphgo => Sha1::digest(content).to_vec(),
            IdxDescription::Sha1gitFmphgo => {
                let mut hasher = Sha1::new();
                hasher.update(b"blob ");
                hasher.update(content.len().to_string().as_bytes());
                hasher.update(b"\0");
                hasher.update(content);
                hasher.finalize().to_vec()
            }
            IdxDescription::Blake2Fmphgo => Blake2s256::digest(content).to_vec(),
            IdxDescription::Sha256Fmphgo => Sha256::digest(content).to_vec(),
        })
        .map(Bytes::from)
        .collect()
}

#[cfg(test)]
mod tests {
    use hex_literal::hex;

    use super::*;

    #[test]
    fn test_sha1_git() {
        // checking the values against `swh identify`
        assert_eq!(
            build_keys(&[], &[IdxDescription::Sha1gitFmphgo]),
            vec![hex!("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").to_vec()]
        );

        let hello = b"println(\"Hello World!\");\n".to_vec();
        assert_eq!(
            build_keys(&hello, &[IdxDescription::Sha1gitFmphgo]),
            vec![hex!("2ecac3196334a6d163f00484ffaffea262516a6e").to_vec()]
        );
    }
}