dia-args 0.59.4

For handling command line arguments
Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Dia-Args

Copyright (C) 2018-2019, 2021-2023  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2019".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Helper binary

#![warn(missing_docs)]

#![allow(clippy::bool_comparison)]
#![allow(clippy::cognitive_complexity)]
#![allow(clippy::match_bool)]

/// # Wrapper for format!(), which prefixes your message with: crate::TAG, module_path!(), line!()
macro_rules! __ { ($($arg: tt)+) => {
    format!("[{tag}][{module_path}-{line}] {fmt}", tag=crate::TAG, module_path=module_path!(), line=line!(), fmt=format!($($arg)+))
};}

mod template_type;
mod templates;

use {
    std::{
        borrow::Cow,
        env,
        io::{Error, ErrorKind},
        path::Path,
        process,
        time::{SystemTime, UNIX_EPOCH},
    },
    dia_args::{
        Args,
        Result,
        licenses::gnu_gpl,
    },
    dia_time::Time,
    template_type::TemplateType,
    zeros::keccak::{self, Hash, Keccak},
};

const TAG: &str = dia_args::TAG;

const CMD_HELP: &str = "help";
const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");

const CMD_VERSION: &str = "version";
const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");

const CMD_LICENSES: &str = "licenses";
const CMD_LICENSES_DOCS: Cow<str> = Cow::Borrowed("Prints licenses and exits.");

const CMD_MAKE_TEMPLATE: &str = "make-template";
macro_rules! cmd_make_template_doc_template { () => { concat!(
    "Makes template and prints it to stdout.\n\n",
    "This command will:\n\n",
    "- Generate some constants, such as NAME, CODE_NAME, ID(1), VERSION, RELEASE_DATE, TAG...\n",
    "- Generate some functions in case your type is \"{}\".\n\n",
    "(1) To generate crate ID, the program will collect these data:\n\n",
    "- Current time.\n",
    "- Some bytes from /dev/urandom (on Unix).\n",
    "- Current directory path and some of its sub files' paths.\n",
    "- Temporary directory path and some of its sub files' paths.\n",
    "- Environment variables.\n",
    "- ...\n\n",
    "Then those data will be fed to an SHA3-512 hasher. Final result will be the hash.\n",
)}}

const OPTION_TEMPLATE_TYPE: &[&str] = &["--type"];
const OPTION_TEMPLATE_TYPE_DOCS: Cow<str> = Cow::Borrowed("Template type.");
const OPTION_TEMPLATE_TYPE_VALUES: &[TemplateType] = &[TemplateType::LIB, TemplateType::BIN];
const OPTION_TEMPLATE_TYPE_DEFAULT: TemplateType = TemplateType::LIB;

const OPTION_NAME: &[&str] = &["--name"];
const OPTION_NAME_DOCS: Cow<str> = Cow::Borrowed("Crate name.");

const OPTION_CODE_NAME: &[&str] = &["--code-name"];
macro_rules! option_code_name_doc_template { () => { concat!(
    "Crate code name.\n\n",
    "If not provided, it will be made from {option_name:?} option, by lowering its content and replacing white spaces with hyphens (\"-\").",
)}}

/// # Main
fn main() {
    if let Err(err) = run() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

/// # Runs the program
fn run() -> Result<()> {
    let args = dia_args::parse()?;
    match args.cmd() {
        Some(CMD_HELP) => {
            ensure_args_are_empty(args.into_sub_cmd().1)?;
            print_help()
        },
        Some(CMD_VERSION) => {
            ensure_args_are_empty(args.into_sub_cmd().1)?;
            print_version()
        },
        Some(CMD_MAKE_TEMPLATE) => make_template(args.into_sub_cmd().1),
        Some(CMD_LICENSES) => print_licenses(args.into_sub_cmd().1),
        Some(other) => Err(Error::new(ErrorKind::InvalidInput, format!("Unknown command: {:?}", other))),
        None => Err(Error::new(ErrorKind::Other, "Not implemented yet")),
    }
}

/// # Ensures arguments are empty
fn ensure_args_are_empty<A>(args: A) -> Result<()> where A: AsRef<Args> {
    let args = args.as_ref();
    if args.is_empty() {
        Ok(())
    } else {
        Err(Error::new(ErrorKind::InvalidInput, format!("Unknown arguments: {:?}", args)))
    }
}

/// # Makes version string
fn make_version_string<'a>() -> Cow<'a, str> {
    format!("{} {} {:?}", dia_args::NAME, dia_args::VERSION, dia_args::RELEASE_DATE).into()
}

/// # Prints version
fn print_version() -> Result<()> {
    println!("{}", make_version_string());
    Ok(())
}

/// # Prints licenses
fn print_licenses(args: Args) -> Result<()> {
    ensure_args_are_empty(args)?;

    dia_args::lock_write_out(format!(
        "{copying}\n\n================================================================\n\n{copying_lesser}\n",
        copying=include_str!("../../../COPYING"),
        copying_lesser=include_str!("../../../COPYING.LESSER"),
    ));

    Ok(())
}

/// # Prints help
fn print_help() -> Result<()> {
    use dia_args::docs::{Cmd, Docs, NO_VALUES, Option, Project};

    let commands = Some(dia_args::make_cmds![
        Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
        Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
        Cmd::new(CMD_LICENSES, CMD_LICENSES_DOCS, None),
        Cmd::new(
            CMD_MAKE_TEMPLATE, format!(cmd_make_template_doc_template!(), TemplateType::BIN).into(),
            Some(dia_args::make_options![
                Option::new(
                    OPTION_TEMPLATE_TYPE, false, OPTION_TEMPLATE_TYPE_VALUES, Some(OPTION_TEMPLATE_TYPE_DEFAULT), OPTION_TEMPLATE_TYPE_DOCS,
                ),
                Option::new(OPTION_NAME, true, NO_VALUES, None, OPTION_NAME_DOCS),
                Option::new(
                    OPTION_CODE_NAME, false, NO_VALUES, None, format!(option_code_name_doc_template!(), option_name=OPTION_NAME).into(),
                ),
            ]),
        ),
    ]);
    let project = Some(Project::new(
        "https://bitbucket.org/de-marco/dia-args",
        "GNU Lesser General Public License, either version 3, or (at your option) any later version",
        None,
    ));

    let mut docs = Docs::new(
        make_version_string(),
        gnu_gpl::make_short_terminal_notice(include_str!("templates.rs"), dia_args::NAME, CMD_LICENSES)?.into(),
    );
    docs.commands = commands;
    docs.project = project;
    docs.print()?;

    Ok(())
}

/// # Makes template
fn make_template(mut args: Args) -> Result<()> {
    use templates::{
        CRATE_CODE_NAME_PLACEHOLDER, CRATE_ID_PLACEHOLDER, CRATE_NAME_PLACEHOLDER, DAY_PLACEHOLDER, MONTH_PLACEHOLDER, TAG_ID_PLACEHOLDER,
        YEAR_PLACEHOLDER, USES_PLACEHOLDER,
    };

    let r#type = args.take(OPTION_TEMPLATE_TYPE)?.unwrap_or(OPTION_TEMPLATE_TYPE_DEFAULT);

    let name = args.take::<String>(OPTION_NAME)?.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("Missing {:?}", OPTION_NAME)))?;
    if name.is_empty() {
        return Err(Error::new(ErrorKind::InvalidInput, format!("{:?} is empty", OPTION_NAME)));
    }

    let code_name = match args.take::<String>(OPTION_CODE_NAME)? {
        Some(code_name) => code_name.trim().to_string(),
        None => name.to_lowercase().trim().split_whitespace().collect::<Vec<_>>().join(concat!('-')),
    };
    if code_name.is_empty() {
        return Err(Error::new(ErrorKind::InvalidInput, format!("{:?} is empty", OPTION_CODE_NAME)));
    }

    ensure_args_are_empty(args)?;

    let (id, prefix) = generate_crate_id()?;
    let time = Time::make_local()?;
    println!("{}", templates::header());
    println!(
        "{}",
        templates::identifiers()
            .replace(CRATE_NAME_PLACEHOLDER, &name).replace(CRATE_CODE_NAME_PLACEHOLDER, &code_name)
            .replace(CRATE_ID_PLACEHOLDER, &id).replace(TAG_ID_PLACEHOLDER, &prefix)
            .replace(YEAR_PLACEHOLDER, &time.year().to_string())
            .replace(MONTH_PLACEHOLDER, &time.month().order().to_string())
            .replace(DAY_PLACEHOLDER, &time.day().to_string())
            .replace(
                USES_PLACEHOLDER,
                &if r#type == TemplateType::BIN { Cow::Owned(format!("\n{}", templates::uses())) } else { Cow::Borrowed(concat!()) },
            )
    );
    if r#type == TemplateType::BIN {
        println!("{}", templates::footer());
    }

    Ok(())
}

/// # Generates crate ID and its prefix
fn generate_crate_id() -> Result<(String, String)> {
    const PREFIX_LEN: usize = 8;
    macro_rules! tab { () => { concat!(' ', ' ', ' ', ' ') }}

    let hex = {
        let mut keccak = Hash::Sha3_512.new_keccak();

        // Time
        let time = match SystemTime::now().duration_since(UNIX_EPOCH) {
            Ok(d) => d,
            Err(e) => e.duration(),
        };
        for n in &[time.as_secs() as u128, time.as_millis(), time.as_micros(), time.as_nanos()] {
            keccak.update(&n.to_ne_bytes());
        }

        // /dev/urandom
        #[cfg(unix)]
        keccak.update(keccak.hash().hash_urandom(512)?);

        // Some paths
        for path in &[env::current_dir(), env::current_exe(), Ok(env::temp_dir())] {
            if let Ok(path) = path {
                keccak.update(path.display().to_string());
                if path.is_dir() {
                    collect_hashes_from_file_paths(path, &mut keccak);
                }
            }
        }

        // Environment variables
        for (k, v) in env::vars_os() {
            for s in [k, v] {
                #[cfg(unix)] {
                    use std::os::unix::prelude::OsStringExt;
                    keccak.update(s.into_vec());
                }
                #[cfg(not(unix))]
                if let Some(s) = s.to_str() {
                    keccak.update(s);
                } else {
                    keccak.update(format!("{:?}", s));
                }
            }
        }

        keccak.finish_as_hex()
    };

    let hex_len = hex.len();
    if hex_len > PREFIX_LEN && hex_len / 2 == 64 {
        let prefix = hex[..PREFIX_LEN].to_string();

        let mut id = String::with_capacity(hex_len.saturating_mul(2));
        id += concat!("concat!(\n", tab!(), '"');
        for (i, c) in hex.chars().enumerate() {
            id.push(c);

            let k = i + 1;
            if k != hex_len && k % 8 == 0 {
                id.push('-');
            }

            if k % 64 == 0 {
                id += concat!('"', ',', '\n');
                id += if k < hex_len {
                    concat!(tab!(), '"')
                } else {
                    concat!(')')
                };
            }
        }

        Ok((id, prefix))
    } else {
        Err(Error::new(ErrorKind::Other, __!("Internal error")))
    }
}

/// # Finds some files in given directory and feeds their paths to given keccak
fn collect_hashes_from_file_paths<P>(dir: P, keccak: &mut Keccak<{keccak::Type::Function.id()}>) where P: AsRef<Path> {
    const MAX_FILES: usize = 1_000;

    let file_discovery = match dia_files::find_files(dir, true, dia_files::filter::AllPaths::new()) {
        Ok(file_discovery) => file_discovery,
        Err(err) => {
            keccak.update(err.to_string());
            return;
        },
    };
    for (i, file) in file_discovery.enumerate() {
        match file {
            Ok(file) => {
                #[cfg(unix)] {
                    use std::os::unix::prelude::OsStrExt;
                    keccak.update(file.as_os_str().as_bytes());
                }
                #[cfg(not(unix))]
                keccak.update(format!("{:?}", file));
            },
            Err(err) => drop(keccak.update(err.to_string())),
        };
        if i >= MAX_FILES {
            return;
        }
    }
}