1#![allow(unused_must_use)] use clap::{Arg, ArgAction, Command};
9use std::ffi::OsString;
10use std::fs;
11use std::io::{ErrorKind, Write};
12use uucore::display::Quotable;
13use uucore::error::{UResult, UUsageError, set_exit_code};
14use uucore::format_usage;
15use uucore::translate;
16
17enum Mode {
19 Default, Basic, Extra, Both, }
24
25mod options {
26 pub const POSIX: &str = "posix";
27 pub const POSIX_SPECIAL: &str = "posix-special";
28 pub const PORTABILITY: &str = "portability";
29 pub const PATH: &str = "path";
30}
31
32const POSIX_PATH_MAX: usize = 256;
34const POSIX_NAME_MAX: usize = 14;
35
36#[uucore::main(no_signals)]
37pub fn uumain(args: impl uucore::Args) -> UResult<()> {
38 let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
39
40 let is_posix = matches.get_flag(options::POSIX);
42 let is_posix_special = matches.get_flag(options::POSIX_SPECIAL);
43 let is_portability = matches.get_flag(options::PORTABILITY);
44
45 let mode = if (is_posix && is_posix_special) || is_portability {
46 Mode::Both
47 } else if is_posix {
48 Mode::Basic
49 } else if is_posix_special {
50 Mode::Extra
51 } else {
52 Mode::Default
53 };
54
55 let paths = matches.get_many::<OsString>(options::PATH);
57 if paths.is_none() {
58 return Err(UUsageError::new(
59 1,
60 translate!("pathchk-error-missing-operand"),
61 ));
62 }
63
64 let mut res = true;
67 for p in paths.unwrap() {
68 let path_str = p.to_string_lossy();
69 let mut path = Vec::new();
70 for path_segment in path_str.split('/') {
71 path.push(path_segment.to_string());
72 }
73 res &= check_path(&mode, &path);
74 }
75
76 if !res {
78 set_exit_code(1);
79 }
80 Ok(())
81}
82
83pub fn uu_app() -> Command {
84 Command::new(uucore::util_name())
85 .version(uucore::crate_version!())
86 .help_template(uucore::localized_help_template(uucore::util_name()))
87 .about(translate!("pathchk-about"))
88 .override_usage(format_usage(&translate!("pathchk-usage")))
89 .infer_long_args(true)
90 .arg(
91 Arg::new(options::POSIX)
92 .short('p')
93 .help(translate!("pathchk-help-posix"))
94 .action(ArgAction::SetTrue),
95 )
96 .arg(
97 Arg::new(options::POSIX_SPECIAL)
98 .short('P')
99 .help(translate!("pathchk-help-posix-special"))
100 .action(ArgAction::SetTrue),
101 )
102 .arg(
103 Arg::new(options::PORTABILITY)
104 .long(options::PORTABILITY)
105 .help(translate!("pathchk-help-portability"))
106 .action(ArgAction::SetTrue),
107 )
108 .arg(
109 Arg::new(options::PATH)
110 .hide(true)
111 .action(ArgAction::Append)
112 .value_hint(clap::ValueHint::AnyPath)
113 .value_parser(clap::value_parser!(OsString)),
114 )
115}
116
117fn check_path(mode: &Mode, path: &[String]) -> bool {
119 match *mode {
120 Mode::Basic => check_basic(path),
121 Mode::Extra => check_default(path) && check_extra(path),
122 Mode::Both => check_basic(path) && check_extra(path),
123 Mode::Default => check_default(path),
124 }
125}
126
127fn check_basic(path: &[String]) -> bool {
129 let joined_path = path.join("/");
130 let total_len = joined_path.len();
131 if total_len > POSIX_PATH_MAX {
133 writeln!(
134 std::io::stderr(),
135 "{}",
136 translate!("pathchk-error-posix-path-length-exceeded", "limit" => POSIX_PATH_MAX, "length" => total_len, "path" => joined_path)
137 );
138 return false;
139 } else if total_len == 0 {
140 writeln!(
141 std::io::stderr(),
142 "{}",
143 translate!("pathchk-error-empty-file-name")
144 );
145 return false;
146 }
147 for p in path {
149 let component_len = p.len();
150 if component_len > POSIX_NAME_MAX {
151 writeln!(
152 std::io::stderr(),
153 "{}",
154 translate!("pathchk-error-posix-name-length-exceeded", "limit" => POSIX_NAME_MAX, "length" => component_len, "component" => p.quote())
155 );
156 return false;
157 }
158 if !check_portable_chars(p) {
159 return false;
160 }
161 }
162 check_searchable(&joined_path)
164}
165
166fn check_extra(path: &[String]) -> bool {
168 for p in path {
170 if p.starts_with('-') {
171 writeln!(
172 std::io::stderr(),
173 "{}",
174 translate!("pathchk-error-leading-hyphen", "component" => p.quote())
175 );
176 return false;
177 }
178 }
179 if path.join("/").is_empty() {
181 writeln!(
182 std::io::stderr(),
183 "{}",
184 translate!("pathchk-error-empty-file-name")
185 );
186 return false;
187 }
188 true
189}
190
191fn check_default(path: &[String]) -> bool {
193 let joined_path = path.join("/");
194 let total_len = joined_path.len();
195 if total_len > libc::PATH_MAX as usize {
197 writeln!(
198 std::io::stderr(),
199 "{}",
200 translate!("pathchk-error-path-length-exceeded", "limit" => libc::PATH_MAX, "length" => total_len, "path" => joined_path.quote())
201 );
202 return false;
203 }
204 if total_len == 0 {
205 if fs::symlink_metadata(&joined_path).is_err() {
210 writeln!(
211 std::io::stderr(),
212 "{}",
213 translate!("pathchk-error-empty-path-not-found")
214 );
215 return false;
216 }
217 }
218
219 for p in path {
221 let component_len = p.len();
222 if component_len > libc::FILENAME_MAX as usize {
223 writeln!(
224 std::io::stderr(),
225 "{}",
226 translate!("pathchk-error-name-length-exceeded", "limit" => libc::FILENAME_MAX, "length" => component_len, "component" => p.quote())
227 );
228 return false;
229 }
230 }
231 check_searchable(&joined_path)
233}
234
235fn check_searchable(path: &str) -> bool {
237 match fs::symlink_metadata(path) {
239 Ok(_) => true,
240 Err(e) => {
241 if e.kind() == ErrorKind::NotFound {
242 true
243 } else {
244 writeln!(std::io::stderr(), "{e}");
245 false
246 }
247 }
248 }
249}
250
251fn check_portable_chars(path_segment: &str) -> bool {
253 const VALID_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
254 for (i, ch) in path_segment.as_bytes().iter().enumerate() {
255 if !VALID_CHARS.contains(ch) {
256 let invalid = path_segment[i..].chars().next().unwrap();
257 writeln!(
258 std::io::stderr(),
259 "{}",
260 translate!("pathchk-error-nonportable-character", "character" => invalid, "component" => path_segment.quote())
261 );
262 return false;
263 }
264 }
265 true
266}