arch-commando 0.6.0

A fast, locate-like utility to search for commands in Arch Linux repos
use std::{
    fs::File,
    io::{stdout, BufRead, BufReader, Read, Write},
    path::Path,
    process::exit,
};

use byteorder::ReadBytesExt;

use crate::{color, error::CommandoError};

const MAGIC: [u8; 8] = [0x7F, 0x43, 0x4F, 0x4D, 0x4D, 0x44, 0x42, 0x7F];
const CDB_VERSION: u32 = 1;

/// A single entry to be written inside a CDB file
pub struct CDBEntry {
    /// Binary name
    pub command: String,

    /// List of package names that provide `Self.command`
    pub packages: Vec<String>,
}

/// Sorts and writes vector of CDBEntry to a CDB file
pub fn create_cdb<P: AsRef<Path>>(mut data: Vec<CDBEntry>, path: P) -> Result<(), CommandoError> {
    data.sort_unstable_by_key(|entry| entry.command.len());

    let path = path.as_ref();
    let mut file = File::create(path)
        .map_err(|err| CommandoError::CdbCreation(path.to_str().unwrap().to_owned(), err))?;

    file.write_all(&MAGIC)?; // Write magic_number
    file.write_all(&CDB_VERSION.to_le_bytes())?; // Write version_number
    file.sync_all()?; // Sync

    for entry in data {
        let command = &entry.command;
        file.write_all(&[command.len().clamp(0, u8::MAX as usize) as u8])?; // Write command_length
        file.write_all(command.get(..255).unwrap_or(command).as_bytes())?; // Write command_name
                                                                           // );

        let mut package = entry.packages.join("\n");
        package.push('\n');

        if let Some(e) = package.get(..(u32::MAX - 1) as usize) {
            package = e.to_string();
            package.push('\n');
        }

        file.write_all(&(package.len().clamp(0, u32::MAX as usize) as u32).to_le_bytes())?; // Write package_length
        file.write_all(package.as_bytes())?; // Write package_name(s) + newline
        file.write_all(&[0x3])?; // Write ETX
    }

    Ok(())
}

/// Search for command in CDB file
pub fn search_in_cdb<S: AsRef<str>, P: AsRef<Path>>(
    command: S,
    path: P,
    verbose: bool,
) -> Result<(), CommandoError> {
    let command = command.as_ref();

    let mut file = BufReader::new(File::open(path).map_err(CommandoError::CdbOpen)?);

    let mut magic_buf = [0u8; 8];
    file.read_exact(&mut magic_buf)?;
    match magic_buf {
        MAGIC => Ok(()),
        _ => Err(CommandoError::BadMagic),
    }?;

    let mut version_number = [0u8, 0, 0, 0];
    file.read_exact(&mut version_number)?;
    let version_number = u32::from_le_bytes(version_number);

    #[allow(unreachable_patterns)] // CDB_VERSION might be set to zero in dev builds.
    match version_number {
        0 => log::warn!("Reading unstable CDB file, proper behaviour is not guaranteed"),
        CDB_VERSION => {}
        version => {
            return Err(CommandoError::BadVersion {
                expected: CDB_VERSION,
                got: version,
            });
        }
    }

    #[cfg(debug_assertions)]
    let mut current_address: u32 = 12;

    macro_rules! increase_address {
        () => {
            #[cfg(debug_assertions)]
            #[allow(unused_assignments)]
            {
                current_address += 1;
            }
        };

        ($increment:expr) => {
            #[cfg(debug_assertions)]
            #[allow(unused_assignments)]
            {
                current_address += $increment;
            }
        };
    }

    #[inline]
    fn cmd_not_found(command: &str, verbose: bool) -> ! {
        if verbose {
            println!(
                "{red}Command `{yellow}{command}{red}` not found in commando database{reset}",
                red = color!(red),
                yellow = color!(yellow),
                reset = color!(reset)
            );
        }

        exit(127);
    }

    loop {
        let command_length = match file.read_u8() {
            Ok(l) => l,
            _ => cmd_not_found(command, verbose),
        };

        increase_address!();

        macro_rules! skip_package {
            () => {
                let mut package_length = [0u8, 0, 0, 0];
                file.read_exact(&mut package_length)?;
                increase_address!(4);

                let package_length = u32::from_le_bytes(package_length);

                file.seek_relative((package_length as i64) + 1)?;

                increase_address!(package_length + 1);
            };
        }

        if command_length > command.len() as u8 {
            cmd_not_found(command, verbose)
        }

        if command_length != command.len() as u8 {
            file.seek_relative(command_length.into())?;

            increase_address!(command_length as u32);

            skip_package!();

            continue;
        }

        let mut command_name = Vec::with_capacity(command.len());
        #[allow(clippy::uninit_vec)]
        unsafe {
            command_name.set_len(command.len())
        }

        #[cfg(debug_assertions)]
        log::debug!("Reading command name at address: {current_address:#X}");

        file.read_exact(&mut command_name)?;
        let command_name = String::from_utf8(command_name).unwrap();

        log::debug!("Command name: '{command_name}'");

        if command_name != command {
            skip_package!();

            continue;
        }

        #[cfg(debug_assertions)]
        {
            log::debug!("Command found");
            let mut package_length = [0u8, 0, 0, 0];
            file.read_exact(&mut package_length)?;

            let package_length = u32::from_le_bytes(package_length);
            increase_address!(package_length);
        }

        #[cfg(not(debug_assertions))]
        file.seek_relative(4)?;

        increase_address!(4);

        let mut packages = Vec::with_capacity(20);

        let mut stdout = stdout();
        file.read_until(0x03, &mut packages)?;

        let mut packages = packages.as_slice();
        if packages.ends_with(&[0x03]) {
            packages = &packages[..packages.len() - 1];
        }

        if verbose {
            print!(
                "{green}Found command `{yellow}{command}{green}` in the following packages:{white}",
                green = color!(green),
                yellow = color!(yellow),
                white = color!(white),
            );

            let packages = packages.split(|e| e == &b'\n');
            for package in packages {
                if !package.is_empty() || package == b"\n" {
                    stdout.write_all(b"\n\n\t")?;
                    stdout.write_all(package)?;
                }
            }
            stdout.write_all(b"\n")?;
        } else {
            stdout.write_all(packages)?;
        }

        stdout.write_all(color!(reset).as_bytes())?;

        stdout.flush()?;

        break;
    }

    Ok(())
}