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/>.

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

//! # Extensions for [`Args`][struct:Args]
//!
//! [struct:Args]: ../struct.Args.html

use {
    std::{
        fs::{self, File},
        io::{Error, ErrorKind},
        path::{Path, PathBuf},
    },
    crate::{Args, Result},
};

/// # Path kind
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum PathKind {

    /// # Directory
    Directory,

    /// # File
    File,

}

/// # Take option
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum TakeOption {

    /// # Must exist
    MustExist,

    /// # Deny existing
    DenyExisting,

    /// # Just take whatever it is
    Take {

        /// # If the path does not exist, and this flag is `true`, make it
        ///
        /// - If [`PathKind::Directory`][enum:PathKind/Directory] is used, make new directory via
        ///   [`fs::create_dir_all()`][fn:fs/create_dir_all].
        /// - If [`PathKind::File`][enum:PathKind/File] is used, make new empty file via [`File::create()`][fn:File/create].
        ///
        /// [enum:PathKind/Directory]: enum.PathKind.html#variant.Directory
        /// [enum:PathKind/File]: enum.PathKind.html#variant.File
        /// [fn:fs/create_dir_all]: https://doc.rust-lang.org/std/fs/fn.create_dir_all.html
        /// [fn:File/create]: https://doc.rust-lang.org/std/fs/struct.File.html#method.create
        make: bool,

    },

}

/// # Gets a path from arguments
///
/// ## Notes
///
/// Error messages are hard-coded. If you want to handle errors, you can get error kinds.
pub fn get_path(args: &Args, keys: &[&str], kind: PathKind, option: TakeOption) -> Result<Option<PathBuf>> {
    match args.get::<PathBuf>(keys)? {
        Some(path) => handle_path(path, kind, option).map(|p| Some(p)),
        None => Ok(None),
    }
}

/// # Takes a path from arguments
///
/// ## Notes
///
/// Error messages are hard-coded. If you want to handle errors, you can get error kinds.
///
/// ## Examples
///
/// ```
/// use dia_args::{
///     paths::{self, PathKind, TakeOption},
/// };
///
/// let mut args = dia_args::parse_strings(["--input", file!()].iter()).unwrap();
/// let file = paths::take_path(
///     &mut args, &["--input"], PathKind::File, TakeOption::MustExist,
/// )
///     .unwrap().unwrap();
/// assert!(file.is_file());
/// assert!(args.is_empty());
/// ```
pub fn take_path(args: &mut Args, keys: &[&str], kind: PathKind, option: TakeOption) -> Result<Option<PathBuf>> {
    match args.take::<PathBuf>(keys)? {
        Some(path) => handle_path(path, kind, option).map(|p| Some(p)),
        None => Ok(None),
    }
}

/// # Handles path
///
/// This function verifies path kind and handles option. New directory or new file will be made if necessary. On success, it returns the input
/// path.
///
/// ## Examples
///
/// ```
/// use dia_args::paths::{self, PathKind, TakeOption};
///
/// assert_eq!(
///     paths::handle_path(file!(), PathKind::File, TakeOption::MustExist)?,
///     file!(),
/// );
///
/// # Ok::<_, std::io::Error>(())
/// ```
pub fn handle_path<P>(path: P, kind: PathKind, option: TakeOption) -> Result<P> where P: AsRef<Path> {
    {
        let path = path.as_ref();
        if match option {
            TakeOption::MustExist => true,
            TakeOption::Take { .. } if path.exists() => true,
            _ => false,
        } {
            match kind {
                PathKind::Directory => if path.is_dir() == false {
                    return Err(Error::new(ErrorKind::InvalidInput, format!("Not a directory: {:?}", path)));
                },
                PathKind::File => if path.is_file() == false {
                    return Err(Error::new(ErrorKind::InvalidInput, format!("Not a file: {:?}", path)));
                },
            };
        }
        match option {
            TakeOption::MustExist => if path.exists() == false {
                return Err(Error::new(ErrorKind::NotFound, format!("Not found: {:?}", path)));
            },
            TakeOption::DenyExisting => if path.exists() {
                return Err(Error::new(ErrorKind::AlreadyExists, format!("Already exists: {:?}", path)));
            },
            TakeOption::Take { make } => if make && path.exists() == false {
                match kind {
                    PathKind::Directory => fs::create_dir_all(&path)?,
                    PathKind::File => drop(File::create(&path)?),
                };
            },
        };
    }
    Ok(path)
}

#[test]
fn test_take_path() {
    const KEYS: &[&str] = &["--path"];

    let mut args = crate::parse_strings([&KEYS[0], file!()].iter()).unwrap();
    assert!(take_path(&mut args, KEYS, PathKind::File, TakeOption::MustExist).unwrap().is_some());
    assert!(args.is_empty());

    let mut args = crate::parse_strings([&KEYS[0], file!()].iter()).unwrap();
    assert_eq!(get_path(&mut args, KEYS, PathKind::File, TakeOption::DenyExisting).unwrap_err().kind(), ErrorKind::AlreadyExists);
    assert!(args.is_empty() == false);

    let mut args = crate::parse_strings([&KEYS[0], file!()].iter()).unwrap();
    assert_eq!(take_path(&mut args, KEYS, PathKind::Directory, TakeOption::Take { make: false }).unwrap_err().kind(), ErrorKind::InvalidInput);
    assert!(args.is_empty());
}

#[cfg(unix)]
const INVALID_FILE_NAME_CHARS: &[char] = &[];

#[cfg(not(unix))]
const INVALID_FILE_NAME_CHARS: &[char] = &['^', '?', '%', '*', ':', '|', '"', '<', '>'];

/// # Verifies _non-existing_ file name
///
/// ## Notes
///
/// - Maximum lengths are different across platforms. If you do not provide a value for maximum length, `1024` will be used.
/// - Slashes `\/`, leading/trailing white space(s) and line breaks are _invalid_ characters.
/// - Non-ASCII characters are _allowed_.
/// - This function behaves differently across platforms. For example: on Windows `?` is not allowed, but on Unix it's ok.
///
/// Returning value is the input name.
///
/// ## References
///
/// - <https://en.wikipedia.org/wiki/Filename>
/// - <https://en.wikipedia.org/wiki/ASCII>
pub fn verify_ne_file_name<S>(name: S, max_len: Option<usize>) -> Result<S> where S: AsRef<str> {
    {
        let name = name.as_ref();

        if name.len() > max_len.unwrap_or(1024) {
            return Err(Error::new(ErrorKind::InvalidInput, "File name is too long"));
        }
        if name.is_empty() {
            return Err(Error::new(ErrorKind::InvalidInput, "File name is empty"));
        }
        if name.trim().len() != name.len() {
            return Err(Error::new(ErrorKind::InvalidInput, "File name contains leading or trailing white space(s)"));
        }
        if name.chars().any(|c| if c.is_ascii() {
            match c as u8 {
                0..=31 | 127 | b'\\' | b'/' => true,
                _ => INVALID_FILE_NAME_CHARS.contains(&c),
            }
        } else {
            false
        }) {
            return Err(Error::new(ErrorKind::InvalidInput, "File name contains invalid character(s)"));
        }
    }

    Ok(name)
}