createrepo_rs 0.1.4

🦀 Pure Rust implementation of createrepo_c — generates RPM repository metadata (repodata). Drop-in replacement with identical output, zero FFI.
Documentation
use std::io::Write;
use std::path::Path;

use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;

use crate::compression::{bzip2_compress, gzip_compress, xz_compress, zstd_compress};
use crate::types::{CompressionType, Package, PackageFile};
use crate::xml::error::XmlError;

const METADATA_NS: &str = "http://linux.duke.edu/metadata/filelists";

pub fn dump_filelists_xml(
    packages: &[Package],
    filelists_ext: bool,
    pretty: bool,
) -> Result<Vec<u8>, XmlError> {
    let estimated = packages.len() * 256;
    let mut writer = if pretty {
        Writer::new_with_indent(Vec::with_capacity(estimated), b' ', 2)
    } else {
        Writer::new(Vec::with_capacity(estimated))
    };

    writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;

    let mut filelists_start = BytesStart::new("filelists");
    filelists_start.push_attribute(("xmlns", METADATA_NS));
    let package_count = packages.len().to_string();
    filelists_start.push_attribute(("packages", package_count.as_str()));
    writer.write_event(Event::Start(filelists_start))?;

    for package in packages {
        write_package_element(&mut writer, package, filelists_ext)?;
    }

    writer.write_event(Event::End(BytesEnd::new("filelists")))?;

    Ok(writer.into_inner())
}

pub fn dump_filelists(
    packages: &[Package],
    output: &Path,
    compression: CompressionType,
    pretty: bool,
) -> Result<(), XmlError> {
    let xml_content = dump_filelists_xml(packages, false, pretty)?;

    if compression == CompressionType::None {
        std::fs::write(output, xml_content)?;
    } else {
        let compressed = compress_bytes(&xml_content, compression)?;
        std::fs::write(output, compressed)?;
    }

    Ok(())
}

pub fn dump_filelists_ext(
    packages: &[Package],
    output: &Path,
    compression: CompressionType,
    pretty: bool,
) -> Result<(), XmlError> {
    let xml_content = dump_filelists_xml(packages, true, pretty)?;

    if compression == CompressionType::None {
        std::fs::write(output, xml_content)?;
    } else {
        let compressed = compress_bytes(&xml_content, compression)?;
        std::fs::write(output, compressed)?;
    }

    Ok(())
}

fn write_package_element<W: Write>(
    writer: &mut Writer<W>,
    package: &Package,
    filelists_ext: bool,
) -> Result<(), XmlError> {
    let mut pkg_start = BytesStart::new("package");
    pkg_start.push_attribute(("pkgid", package.pkgid.as_str()));
    pkg_start.push_attribute(("name", package.name.as_str()));
    pkg_start.push_attribute(("arch", package.arch.as_str()));
    writer.write_event(Event::Start(pkg_start))?;

    let _ = write_version_element(writer, package);

    if filelists_ext {
        let _ = write_checksum_element(writer, package);
    }

    let _ = write_file_elements(writer, &package.files, filelists_ext);

    writer.write_event(Event::End(BytesEnd::new("package")))?;
    Ok(())
}

fn write_version_element<W: Write>(
    writer: &mut Writer<W>,
    package: &Package,
) -> Result<(), XmlError> {
    let mut version_start = BytesStart::new("version");
    let epoch_val = package.epoch.unwrap_or(0);
    version_start.push_attribute(("epoch", epoch_val.to_string().as_str()));
    version_start.push_attribute(("ver", package.version.as_str()));
    version_start.push_attribute(("rel", package.release.as_str()));
    writer.write_event(Event::Empty(version_start))?;
    Ok(())
}

fn write_checksum_element<W: Write>(
    writer: &mut Writer<W>,
    _package: &Package,
) -> Result<(), XmlError> {
    let mut checksum_start = BytesStart::new("checksum");
    checksum_start.push_attribute(("type", "sha256"));
    writer.write_event(Event::Empty(checksum_start))?;
    Ok(())
}

fn write_file_elements<W: Write>(
    writer: &mut Writer<W>,
    files: &[PackageFile],
    _filelists_ext: bool,
) -> Result<(), XmlError> {
    for file in files {
        let mut file_start = BytesStart::new("file");
        let file_type = file.file_type.as_str();
        if !file_type.is_empty() && file_type != "file" {
            file_start.push_attribute(("type", file_type));
        }
        writer.write_event(Event::Start(file_start))?;
        writer.write_event(Event::Text(BytesText::new(&file.path)))?;
        writer.write_event(Event::End(BytesEnd::new("file")))?;
    }
    Ok(())
}

fn compress_bytes(content: &[u8], compression: CompressionType) -> Result<Vec<u8>, XmlError> {
    match compression {
        CompressionType::Gzip => gzip_compress(content, 6).map_err(XmlError::IoError),
        CompressionType::Bzip2 => bzip2_compress(content, 6).map_err(XmlError::IoError),
        CompressionType::Xz => xz_compress(content, 6).map_err(XmlError::IoError),
        CompressionType::Zstd => zstd_compress(content, 6).map_err(XmlError::IoError),
        CompressionType::None => Ok(content.to_vec()),
    }
}