dia_args/
paths.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Args
5
6Copyright (C) 2018-2019, 2021-2025  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2018-2019".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Extensions for [`Args`][struct:Args]
28//!
29//! [struct:Args]: ../struct.Args.html
30
31use {
32    std::{
33        fs::{self, File},
34        io::{Error, ErrorKind},
35        path::{Path, PathBuf},
36    },
37    crate::{Args, Result},
38};
39
40/// # Path kind
41#[derive(Debug, Eq, PartialEq, Hash)]
42pub enum PathKind {
43
44    /// # Directory
45    Directory,
46
47    /// # File
48    File,
49
50}
51
52/// # Take option
53#[derive(Debug, Eq, PartialEq, Hash)]
54pub enum TakeOption {
55
56    /// # Must exist
57    MustExist,
58
59    /// # Deny existing
60    DenyExisting,
61
62    /// # Just take whatever it is
63    Take {
64
65        /// # If the path does not exist, and this flag is `true`, make it
66        ///
67        /// - If [`PathKind::Directory`][enum:PathKind/Directory] is used, make new directory via
68        ///   [`fs::create_dir_all()`][fn:fs/create_dir_all].
69        /// - If [`PathKind::File`][enum:PathKind/File] is used, make new empty file via [`File::create()`][fn:File/create].
70        ///
71        /// [enum:PathKind/Directory]: enum.PathKind.html#variant.Directory
72        /// [enum:PathKind/File]: enum.PathKind.html#variant.File
73        /// [fn:fs/create_dir_all]: https://doc.rust-lang.org/std/fs/fn.create_dir_all.html
74        /// [fn:File/create]: https://doc.rust-lang.org/std/fs/struct.File.html#method.create
75        make: bool,
76
77    },
78
79}
80
81/// # Takes a path from arguments
82///
83/// ## Notes
84///
85/// Error messages are hard-coded. If you want to handle errors, you can get error kinds.
86///
87/// ## Examples
88///
89/// ```
90/// use dia_args::{
91///     paths::{self, PathKind, TakeOption},
92/// };
93///
94/// let mut args = dia_args::parse_strings(["--input", file!()]).unwrap();
95/// let file = paths::take_path(
96///     &mut args, &["--input"], PathKind::File, TakeOption::MustExist,
97/// )
98///     .unwrap().unwrap();
99/// assert!(file.is_file());
100/// assert!(args.is_empty());
101/// ```
102pub fn take_path(args: &mut Args, keys: &[&str], kind: PathKind, option: TakeOption) -> Result<Option<PathBuf>> {
103    match args.take::<PathBuf>(keys)? {
104        Some(path) => handle_path(path, kind, option).map(|p| Some(p)),
105        None => Ok(None),
106    }
107}
108
109/// # Handles path
110///
111/// This function verifies path kind and handles option. New directory or new file will be made if necessary. On success, it returns the input
112/// path.
113///
114/// ## Examples
115///
116/// ```
117/// use dia_args::paths::{self, PathKind, TakeOption};
118///
119/// assert_eq!(
120///     paths::handle_path(file!(), PathKind::File, TakeOption::MustExist)?,
121///     file!(),
122/// );
123///
124/// # Ok::<_, std::io::Error>(())
125/// ```
126pub fn handle_path<P>(path: P, kind: PathKind, option: TakeOption) -> Result<P> where P: AsRef<Path> {
127    {
128        let path = path.as_ref();
129        if match option {
130            TakeOption::MustExist => true,
131            TakeOption::Take { .. } if path.exists() => true,
132            _ => false,
133        } {
134            match kind {
135                PathKind::Directory => if path.is_dir() == false {
136                    return Err(Error::new(ErrorKind::InvalidInput, format!("Not a directory: {:?}", path)));
137                },
138                PathKind::File => if path.is_file() == false {
139                    return Err(Error::new(ErrorKind::InvalidInput, format!("Not a file: {:?}", path)));
140                },
141            };
142        }
143        match option {
144            TakeOption::MustExist => if path.exists() == false {
145                return Err(Error::new(ErrorKind::NotFound, format!("Not found: {:?}", path)));
146            },
147            TakeOption::DenyExisting => if path.exists() {
148                return Err(Error::new(ErrorKind::AlreadyExists, format!("Already exists: {:?}", path)));
149            },
150            TakeOption::Take { make } => if make && path.exists() == false {
151                match kind {
152                    PathKind::Directory => fs::create_dir_all(&path)?,
153                    PathKind::File => drop(File::create(&path)?),
154                };
155            },
156        };
157    }
158    Ok(path)
159}
160
161#[test]
162fn test_take_path() {
163    const KEYS: &[&str] = &["--path"];
164
165    let mut args = crate::parse_strings([&KEYS[0], file!()]).unwrap();
166    assert!(take_path(&mut args, KEYS, PathKind::File, TakeOption::MustExist).unwrap().is_some());
167    assert!(args.is_empty());
168
169    let mut args = crate::parse_strings([&KEYS[0], file!()]).unwrap();
170    assert_eq!(take_path(&mut args, KEYS, PathKind::File, TakeOption::DenyExisting).unwrap_err().kind(), ErrorKind::AlreadyExists);
171    assert!(args.is_empty());
172
173    let mut args = crate::parse_strings([&KEYS[0], file!()]).unwrap();
174    assert_eq!(take_path(&mut args, KEYS, PathKind::Directory, TakeOption::Take { make: false }).unwrap_err().kind(), ErrorKind::InvalidInput);
175    assert!(args.is_empty());
176}
177
178#[cfg(unix)]
179const INVALID_FILE_NAME_CHARS: &[char] = &[];
180
181#[cfg(not(unix))]
182const INVALID_FILE_NAME_CHARS: &[char] = &['^', '?', '%', '*', ':', '|', '"', '<', '>'];
183
184/// # Verifies _non-existing_ file name
185///
186/// ## Notes
187///
188/// - Maximum lengths are different across platforms. If you do not provide a value for maximum length, `1024` will be used.
189/// - Slashes `\/`, leading/trailing white space(s) and line breaks are _invalid_ characters.
190/// - Non-ASCII characters are _allowed_.
191/// - This function behaves differently across platforms. For example: on Windows `?` is not allowed, but on Unix it's ok.
192///
193/// Returning value is the input name.
194///
195/// ## References
196///
197/// - <https://en.wikipedia.org/wiki/Filename>
198/// - <https://en.wikipedia.org/wiki/ASCII>
199pub fn verify_ne_file_name<S>(name: S, max_len: Option<usize>) -> Result<S> where S: AsRef<str> {
200    {
201        let name = name.as_ref();
202
203        if name.len() > max_len.unwrap_or(1024) {
204            return Err(Error::new(ErrorKind::InvalidInput, "File name is too long"));
205        }
206        if name.is_empty() {
207            return Err(Error::new(ErrorKind::InvalidInput, "File name is empty"));
208        }
209        if name.trim().len() != name.len() {
210            return Err(Error::new(ErrorKind::InvalidInput, "File name contains leading or trailing white space(s)"));
211        }
212        if name.chars().any(|c| if c.is_ascii() {
213            match c as u8 {
214                0..=31 | 127 | b'\\' | b'/' => true,
215                _ => INVALID_FILE_NAME_CHARS.contains(&c),
216            }
217        } else {
218            false
219        }) {
220            return Err(Error::new(ErrorKind::InvalidInput, "File name contains invalid character(s)"));
221        }
222    }
223
224    Ok(name)
225}