repodb_parser 0.3.1

Parser for Arch Linux repository DB's
Documentation
// SPDX-FileCopyrightText: 2022-2024 Michael Picht <mipi@fsfe.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::pkg::{Pkg, Pkgs};
use anyhow::anyhow;
use flate2::read::GzDecoder;
use std::{
    fs::{self, File},
    io::BufReader,
    path::{Path, PathBuf},
    str::FromStr,
};
use tar::Archive;
use xz2::read::XzDecoder;

/// Type of a repository DB. It can be either be a gzip or xz compressed tar
/// archive
#[derive(Eq, PartialEq)]
pub enum RepoDBKind {
    Gzip,
    Xz,
}

/// Support creation from a string
impl FromStr for RepoDBKind {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> anyhow::Result<Self> {
        // check file extension and parse repository DB
        match s {
            "gz" => Ok(RepoDBKind::Gzip),
            "xz" => Ok(RepoDBKind::Xz),
            _ => Err(anyhow!(
                "repository DB has unsupported file extension '{}'",
                s
            )),
        }
    }
}

/// Represents a repository DB
pub struct RepoDB {
    path: PathBuf,
    kind: RepoDBKind,
}

impl RepoDB {
    /// Creates a new representation of a repository DB
    pub fn new<P>(db: P) -> anyhow::Result<RepoDB>
    where
        P: AsRef<Path>,
    {
        let err_msg = format!(
            "cannot determine extension of repository DB '{:?}'",
            db.as_ref().display()
        );

        // Normalize path (i.e., follow symlinks, etc.)
        let path = fs::canonicalize(db)?;

        // Derive repo DB kind from path
        let kind = RepoDBKind::from_str(
            path.extension()
                .ok_or_else(|| anyhow!(err_msg.clone()))?
                .to_str()
                .ok_or_else(|| anyhow!(err_msg.clone()))?,
        )?;

        // Note: The let statements above are necessary since the path must be
        //       normalized before the kind is derived. Inlining this to the Ok
        //       statement below would lead to an error
        Ok(RepoDB { path, kind })
    }

    /// Retrieves meta data of the packages of the repository DB and returns it
    /// as B-tree map, sorted by package name
    pub fn packages(&self) -> anyhow::Result<Pkgs> {
        macro_rules! parse_archive {
            ($archive:expr , $pkgs:expr) => {
                let mut pkg: Pkg;
                for entry in ($archive.entries()?).flatten() {
                    if entry.path()?.file_name().unwrap().to_str().unwrap() == "desc" {
                        pkg = Pkg::parse(BufReader::new(entry))?;
                        $pkgs.add(pkg.name.clone(), pkg);
                    }
                }
            };
        }

        let mut pkgs = Pkgs::default();
        let reader = BufReader::new(File::open(&self.path)?);

        if self.kind == RepoDBKind::Gzip {
            parse_archive!(Archive::new(GzDecoder::new(reader)), pkgs);
        } else {
            parse_archive!(Archive::new(XzDecoder::new(reader)), pkgs);
        };

        Ok(pkgs)
    }
}