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}