uu_pathchk/
pathchk.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5#![allow(unused_must_use)] // because we of writeln!
6
7// spell-checker:ignore (ToDO) lstat
8use 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
17// operating mode
18enum Mode {
19    Default, // use filesystem to determine information and limits
20    Basic,   // check basic compatibility with POSIX
21    Extra,   // check for leading dashes and empty names
22    Both,    // a combination of `Basic` and `Extra`
23}
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
32// a few global constants as used in the GNU implementation
33const POSIX_PATH_MAX: usize = 256;
34const POSIX_NAME_MAX: usize = 14;
35
36#[uucore::main]
37pub fn uumain(args: impl uucore::Args) -> UResult<()> {
38    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
39
40    // set working mode
41    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    // take necessary actions
56    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    // free strings are path operands
65    // FIXME: TCS, seems inefficient and overly verbose (?)
66    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    // determine error code
77    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
117/// check a path, given as a slice of it's components and an operating mode
118fn 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
127/// check a path in basic compatibility mode
128fn check_basic(path: &[String]) -> bool {
129    let joined_path = path.join("/");
130    let total_len = joined_path.len();
131    // path length
132    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    // components: character portability and length
148    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    // permission checks
163    check_searchable(&joined_path)
164}
165
166/// check a path in extra compatibility mode
167fn check_extra(path: &[String]) -> bool {
168    // components: leading hyphens
169    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    // path length
180    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
191/// check a path in default mode (using the file system)
192fn check_default(path: &[String]) -> bool {
193    let joined_path = path.join("/");
194    let total_len = joined_path.len();
195    // path length
196    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        // Check whether a file name component is in a directory that is not searchable,
206        // or has some other serious problem. POSIX does not allow "" as a file name,
207        // but some non-POSIX hosts do (as an alias for "."),
208        // so allow "" if `symlink_metadata` (corresponds to `lstat`) does.
209        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    // components: length
220    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    // permission checks
232    check_searchable(&joined_path)
233}
234
235/// check whether a path is or if other problems arise
236fn check_searchable(path: &str) -> bool {
237    // we use lstat, just like the original implementation
238    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
251/// check whether a path segment contains only valid (read: portable) characters
252fn 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}