mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-27 20:25:12 +00:00
Factor file testing out of highlighting
Syntax highlighting wants to underline arguments that are files, and in other cases do disk I/O (such as testing if a command is valid). Factor out this I/O logic to untangle highlighting, and add some tests. No functional change expected.
This commit is contained in:
parent
4f3d6427ce
commit
9785824794
7 changed files with 743 additions and 423 deletions
417
src/highlight/file_tester.rs
Normal file
417
src/highlight/file_tester.rs
Normal file
|
@ -0,0 +1,417 @@
|
|||
// Support for testing whether files exist and have the correct permissions,
|
||||
// to support highlighting.
|
||||
// Because this may perform blocking I/O, we compute results in a separate thread,
|
||||
// and provide them optimistically.
|
||||
use crate::common::{unescape_string, UnescapeFlags, UnescapeStringStyle};
|
||||
use crate::expand::{
|
||||
expand_one, BRACE_BEGIN, BRACE_END, BRACE_SEP, INTERNAL_SEPARATOR, PROCESS_EXPAND_SELF,
|
||||
VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE,
|
||||
};
|
||||
use crate::expand::{expand_tilde, ExpandFlags, HOME_DIRECTORY};
|
||||
use crate::libc::_PC_CASE_SENSITIVE;
|
||||
use crate::operation_context::OperationContext;
|
||||
use crate::path::path_apply_working_directory;
|
||||
use crate::redirection::RedirectionMode;
|
||||
use crate::threads::assert_is_background_thread;
|
||||
use crate::wchar::{wstr, WString, L};
|
||||
use crate::wchar_ext::WExt;
|
||||
use crate::wcstringutil::{
|
||||
string_prefixes_string, string_prefixes_string_case_insensitive, string_suffixes_string,
|
||||
};
|
||||
use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE};
|
||||
use crate::wutil::{
|
||||
dir_iter::DirIter, fish_wcstoi, normalize_path, waccess, wbasename, wdirname, wstat,
|
||||
};
|
||||
use libc::PATH_MAX;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
// This is used only internally to this file, and is exposed only for testing.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PathFlags {
|
||||
// The path must be to a directory.
|
||||
pub require_dir: bool,
|
||||
// Expand any leading tilde in the path.
|
||||
pub expand_tilde: bool,
|
||||
// Normalize directories before resolving, as "cd".
|
||||
pub for_cd: bool,
|
||||
}
|
||||
|
||||
// When a file test is OK, we may also return whether this was a file.
|
||||
// This is used for underlining and is dependent on the particular file test.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct IsFile(pub bool);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct IsErr;
|
||||
|
||||
/// The result of a file test.
|
||||
pub type FileTestResult = Result<IsFile, IsErr>;
|
||||
|
||||
pub struct FileTester<'s> {
|
||||
// The working directory, for resolving paths against.
|
||||
working_directory: WString,
|
||||
// The operation context.
|
||||
ctx: &'s OperationContext<'s>,
|
||||
}
|
||||
|
||||
impl<'s> FileTester<'s> {
|
||||
pub fn new(working_directory: WString, ctx: &'s OperationContext<'s>) -> Self {
|
||||
Self {
|
||||
working_directory,
|
||||
ctx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether a file exists and is readable.
|
||||
/// The input string 'token' is given as an escaped string (as the user may type in).
|
||||
/// If 'prefix' is true, instead check if the path is a prefix of a valid path.
|
||||
/// Returns false on cancellation.
|
||||
pub fn test_path(&self, token: &wstr, prefix: bool) -> bool {
|
||||
// Skip strings exceeding PATH_MAX. See #7837.
|
||||
// Note some paths may exceed PATH_MAX, but this is just for highlighting.
|
||||
if token.len() > (PATH_MAX as usize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unescape the token.
|
||||
let Some(mut token) =
|
||||
unescape_string(token, UnescapeStringStyle::Script(UnescapeFlags::SPECIAL))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Big hack: is_potential_path expects a tilde, but unescape_string gives us HOME_DIRECTORY.
|
||||
// Put it back.
|
||||
if token.char_at(0) == HOME_DIRECTORY {
|
||||
token.as_char_slice_mut()[0] = '~';
|
||||
}
|
||||
|
||||
is_potential_path(
|
||||
&token,
|
||||
prefix,
|
||||
&[self.working_directory.to_owned()],
|
||||
self.ctx,
|
||||
PathFlags {
|
||||
expand_tilde: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Test if the string is a prefix of a valid path we could cd into, or is some other token
|
||||
// we recognize (primarily --help).
|
||||
// If is_prefix is true, we test if the string is a prefix of a valid path we could cd into.
|
||||
pub fn test_cd_path(&self, token: &wstr, is_prefix: bool) -> FileTestResult {
|
||||
let mut param = token.to_owned();
|
||||
if !expand_one(&mut param, ExpandFlags::FAIL_ON_CMDSUBST, self.ctx, None) {
|
||||
// Failed expansion (e.g. may contain a command substitution). Ignore it.
|
||||
return FileTestResult::Ok(IsFile(false));
|
||||
}
|
||||
// Maybe it's just --help.
|
||||
if string_prefixes_string(¶m, L!("--help")) || string_prefixes_string(¶m, L!("-h"))
|
||||
{
|
||||
return FileTestResult::Ok(IsFile(false));
|
||||
}
|
||||
let valid_path = is_potential_cd_path(
|
||||
¶m,
|
||||
is_prefix,
|
||||
&self.working_directory,
|
||||
self.ctx,
|
||||
PathFlags {
|
||||
expand_tilde: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// cd into an invalid path is an error.
|
||||
if valid_path {
|
||||
Ok(IsFile(valid_path))
|
||||
} else {
|
||||
Err(IsErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Test if a the given string is a valid redirection target, given the mode.
|
||||
// Note we return bool, because we never underline redirection targets.
|
||||
pub fn test_redirection_target(&self, target: &wstr, mode: RedirectionMode) -> bool {
|
||||
// Skip targets exceeding PATH_MAX. See #7837.
|
||||
if target.len() > (PATH_MAX as usize) {
|
||||
return false;
|
||||
}
|
||||
let mut target = target.to_owned();
|
||||
if !expand_one(&mut target, ExpandFlags::FAIL_ON_CMDSUBST, self.ctx, None) {
|
||||
// Could not be expanded.
|
||||
return false;
|
||||
}
|
||||
// Ok, we successfully expanded our target. Now verify that it works with this
|
||||
// redirection. We will probably need it as a path (but not in the case of fd
|
||||
// redirections). Note that the target is now unescaped.
|
||||
let target_path = path_apply_working_directory(&target, &self.working_directory);
|
||||
match mode {
|
||||
RedirectionMode::fd => {
|
||||
if target == "-" {
|
||||
return true;
|
||||
}
|
||||
match fish_wcstoi(&target) {
|
||||
Ok(fd) => fd >= 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
RedirectionMode::input | RedirectionMode::try_input => {
|
||||
// Input redirections must have a readable non-directory.
|
||||
// Note we color "try_input" files as errors if they are invalid,
|
||||
// even though it's possible to execute these (replaced via /dev/null).
|
||||
waccess(&target_path, libc::R_OK) == 0
|
||||
&& wstat(&target_path).map_or(false, |md| !md.file_type().is_dir())
|
||||
}
|
||||
RedirectionMode::overwrite | RedirectionMode::append | RedirectionMode::noclob => {
|
||||
if string_suffixes_string(L!("/"), &target) {
|
||||
// Redirections to things that are directories is definitely not
|
||||
// allowed.
|
||||
return false;
|
||||
}
|
||||
// Test whether the file exists, and whether it's writable (possibly after
|
||||
// creating it). access() returns failure if the file does not exist.
|
||||
// TODO: we do not need to compute file_exists for an 'overwrite' redirection.
|
||||
let file_exists;
|
||||
let file_is_writable;
|
||||
match wstat(&target_path) {
|
||||
Ok(md) => {
|
||||
// No err. We can write to it if it's not a directory and we have
|
||||
// permission.
|
||||
file_exists = true;
|
||||
file_is_writable =
|
||||
!md.file_type().is_dir() && waccess(&target_path, libc::W_OK) == 0;
|
||||
}
|
||||
Err(err) => {
|
||||
if err.raw_os_error() == Some(libc::ENOENT) {
|
||||
// File does not exist. Check if its parent directory is writable.
|
||||
let mut parent = wdirname(&target_path).to_owned();
|
||||
|
||||
// Ensure that the parent ends with the path separator. This will ensure
|
||||
// that we get an error if the parent directory is not really a
|
||||
// directory.
|
||||
if !string_suffixes_string(L!("/"), &parent) {
|
||||
parent.push('/');
|
||||
}
|
||||
|
||||
// Now the file is considered writable if the parent directory is
|
||||
// writable.
|
||||
file_exists = false;
|
||||
file_is_writable = waccess(&parent, libc::W_OK) == 0;
|
||||
} else {
|
||||
// Other errors we treat as not writable. This includes things like
|
||||
// ENOTDIR.
|
||||
file_exists = false;
|
||||
file_is_writable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOCLOB means that we must not overwrite files that exist.
|
||||
file_is_writable && !(file_exists && mode == RedirectionMode::noclob)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests whether the specified string cpath is the prefix of anything we could cd to. directories
|
||||
/// is a list of possible parent directories (typically either the working directory, or the
|
||||
/// cdpath). This does I/O!
|
||||
///
|
||||
/// We expect the path to already be unescaped.
|
||||
pub fn is_potential_path(
|
||||
potential_path_fragment: &wstr,
|
||||
at_cursor: bool,
|
||||
directories: &[WString],
|
||||
ctx: &OperationContext<'_>,
|
||||
flags: PathFlags,
|
||||
) -> bool {
|
||||
// This function is expected to be called from the background thread.
|
||||
// But in tests, threads get weird.
|
||||
if cfg!(not(test)) {
|
||||
assert_is_background_thread();
|
||||
}
|
||||
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let require_dir = flags.require_dir;
|
||||
let mut clean_potential_path_fragment = WString::new();
|
||||
let mut has_magic = false;
|
||||
|
||||
let mut path_with_magic = potential_path_fragment.to_owned();
|
||||
if flags.expand_tilde {
|
||||
expand_tilde(&mut path_with_magic, ctx.vars());
|
||||
}
|
||||
|
||||
for c in path_with_magic.chars() {
|
||||
match c {
|
||||
PROCESS_EXPAND_SELF
|
||||
| VARIABLE_EXPAND
|
||||
| VARIABLE_EXPAND_SINGLE
|
||||
| BRACE_BEGIN
|
||||
| BRACE_END
|
||||
| BRACE_SEP
|
||||
| ANY_CHAR
|
||||
| ANY_STRING
|
||||
| ANY_STRING_RECURSIVE => {
|
||||
has_magic = true;
|
||||
}
|
||||
INTERNAL_SEPARATOR => (),
|
||||
_ => clean_potential_path_fragment.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
if has_magic || clean_potential_path_fragment.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't test the same path multiple times, which can happen if the path is absolute and the
|
||||
// CDPATH contains multiple entries.
|
||||
let mut checked_paths = HashSet::new();
|
||||
|
||||
// Keep a cache of which paths / filesystems are case sensitive.
|
||||
let mut case_sensitivity_cache = CaseSensitivityCache::new();
|
||||
|
||||
for wd in directories {
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
let mut abs_path = path_apply_working_directory(&clean_potential_path_fragment, wd);
|
||||
let must_be_full_dir = abs_path.chars().next_back() == Some('/');
|
||||
if flags.for_cd {
|
||||
abs_path = normalize_path(&abs_path, /*allow_leading_double_slashes=*/ true);
|
||||
}
|
||||
|
||||
// Skip this if it's empty or we've already checked it.
|
||||
if abs_path.is_empty() || checked_paths.contains(&abs_path) {
|
||||
continue;
|
||||
}
|
||||
checked_paths.insert(abs_path.clone());
|
||||
|
||||
// If the user is still typing the argument, we want to highlight it if it's the prefix
|
||||
// of a valid path. This means we need to potentially walk all files in some directory.
|
||||
// There are two easy cases where we can skip this:
|
||||
// 1. If the argument ends with a slash, it must be a valid directory, no prefix.
|
||||
// 2. If the cursor is not at the argument, it means the user is definitely not typing it,
|
||||
// so we can skip the prefix-match.
|
||||
if must_be_full_dir || !at_cursor {
|
||||
if let Ok(md) = wstat(&abs_path) {
|
||||
if !at_cursor || md.file_type().is_dir() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We do not end with a slash; it does not have to be a directory.
|
||||
let dir_name = wdirname(&abs_path);
|
||||
let filename_fragment = wbasename(&abs_path);
|
||||
if dir_name == "/" && filename_fragment == "/" {
|
||||
// cd ///.... No autosuggestion.
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Ok(mut dir) = DirIter::new(dir_name) {
|
||||
// Check if we're case insensitive.
|
||||
let do_case_insensitive =
|
||||
fs_is_case_insensitive(dir_name, dir.fd(), &mut case_sensitivity_cache);
|
||||
|
||||
// We opened the dir_name; look for a string where the base name prefixes it.
|
||||
while let Some(entry) = dir.next() {
|
||||
let Ok(entry) = entry else { continue };
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Maybe skip directories.
|
||||
if require_dir && !entry.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if string_prefixes_string(filename_fragment, &entry.name)
|
||||
|| (do_case_insensitive
|
||||
&& string_prefixes_string_case_insensitive(
|
||||
filename_fragment,
|
||||
&entry.name,
|
||||
))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Given a string, return whether it prefixes a path that we could cd into. Return that path in
|
||||
// out_path. Expects path to be unescaped.
|
||||
pub fn is_potential_cd_path(
|
||||
path: &wstr,
|
||||
at_cursor: bool,
|
||||
working_directory: &wstr,
|
||||
ctx: &OperationContext<'_>,
|
||||
mut flags: PathFlags,
|
||||
) -> bool {
|
||||
let mut directories = vec![];
|
||||
|
||||
if string_prefixes_string(L!("./"), path) {
|
||||
// Ignore the CDPATH in this case; just use the working directory.
|
||||
directories.push(working_directory.to_owned());
|
||||
} else {
|
||||
// Get the CDPATH.
|
||||
let cdpath = ctx.vars().get_unless_empty(L!("CDPATH"));
|
||||
let mut pathsv = match cdpath {
|
||||
None => vec![L!(".").to_owned()],
|
||||
Some(cdpath) => cdpath.as_list().to_vec(),
|
||||
};
|
||||
// The current $PWD is always valid.
|
||||
pathsv.push(L!(".").to_owned());
|
||||
|
||||
for mut next_path in pathsv {
|
||||
if next_path.is_empty() {
|
||||
next_path = L!(".").to_owned();
|
||||
}
|
||||
// Ensure that we use the working directory for relative cdpaths like ".".
|
||||
directories.push(path_apply_working_directory(&next_path, working_directory));
|
||||
}
|
||||
}
|
||||
|
||||
// Call is_potential_path with all of these directories.
|
||||
flags.require_dir = true;
|
||||
flags.for_cd = true;
|
||||
is_potential_path(path, at_cursor, &directories, ctx, flags)
|
||||
}
|
||||
|
||||
/// Determine if the filesystem containing the given fd is case insensitive for lookups regardless
|
||||
/// of whether it preserves the case when saving a pathname.
|
||||
///
|
||||
/// Returns:
|
||||
/// false: the filesystem is not case insensitive
|
||||
/// true: the file system is case insensitive
|
||||
pub type CaseSensitivityCache = HashMap<WString, bool>;
|
||||
fn fs_is_case_insensitive(
|
||||
path: &wstr,
|
||||
fd: RawFd,
|
||||
case_sensitivity_cache: &mut CaseSensitivityCache,
|
||||
) -> bool {
|
||||
let mut result = false;
|
||||
if *_PC_CASE_SENSITIVE != 0 {
|
||||
// Try the cache first.
|
||||
match case_sensitivity_cache.entry(path.to_owned()) {
|
||||
Entry::Occupied(e) => {
|
||||
/* Use the cached value */
|
||||
result = *e.get();
|
||||
}
|
||||
Entry::Vacant(e) => {
|
||||
// Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case
|
||||
// sensitive, and a 0 value means case insensitive.
|
||||
let ret = unsafe { libc::fpathconf(fd, *_PC_CASE_SENSITIVE) };
|
||||
result = ret == 0;
|
||||
e.insert(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
|
@ -7,23 +7,17 @@ use crate::ast::{
|
|||
use crate::builtins::shared::builtin_exists;
|
||||
use crate::color::RgbColor;
|
||||
use crate::common::{
|
||||
unescape_string, valid_var_name, valid_var_name_char, UnescapeFlags, ASCII_MAX,
|
||||
EXPAND_RESERVED_BASE, EXPAND_RESERVED_END,
|
||||
valid_var_name, valid_var_name_char, ASCII_MAX, EXPAND_RESERVED_BASE, EXPAND_RESERVED_END,
|
||||
};
|
||||
use crate::complete::complete_wrap_map;
|
||||
use crate::env::Environment;
|
||||
use crate::expand::{
|
||||
expand_one, expand_tilde, expand_to_command_and_args, ExpandFlags, ExpandResultCode,
|
||||
HOME_DIRECTORY, PROCESS_EXPAND_SELF_STR,
|
||||
};
|
||||
use crate::expand::{
|
||||
BRACE_BEGIN, BRACE_END, BRACE_SEP, INTERNAL_SEPARATOR, PROCESS_EXPAND_SELF, VARIABLE_EXPAND,
|
||||
VARIABLE_EXPAND_SINGLE,
|
||||
expand_one, expand_to_command_and_args, ExpandFlags, ExpandResultCode, PROCESS_EXPAND_SELF_STR,
|
||||
};
|
||||
use crate::function;
|
||||
use crate::future_feature_flags::{feature_test, FeatureFlag};
|
||||
use crate::highlight::file_tester::FileTester;
|
||||
use crate::history::{all_paths_are_valid, HistoryItem};
|
||||
use crate::libc::_PC_CASE_SENSITIVE;
|
||||
use crate::operation_context::OperationContext;
|
||||
use crate::output::{parse_color, Outputter};
|
||||
use crate::parse_constants::{
|
||||
|
@ -32,27 +26,16 @@ use crate::parse_constants::{
|
|||
use crate::parse_util::{
|
||||
parse_util_locate_cmdsubst_range, parse_util_slice_length, MaybeParentheses,
|
||||
};
|
||||
use crate::path::{
|
||||
path_apply_working_directory, path_as_implicit_cd, path_get_cdpath, path_get_path,
|
||||
paths_are_same_file,
|
||||
};
|
||||
use crate::redirection::RedirectionMode;
|
||||
use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file};
|
||||
use crate::threads::assert_is_background_thread;
|
||||
use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir};
|
||||
use crate::wchar::{wstr, WString, L};
|
||||
use crate::wchar_ext::WExt;
|
||||
use crate::wcstringutil::{
|
||||
string_prefixes_string, string_prefixes_string_case_insensitive, string_suffixes_string,
|
||||
};
|
||||
use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE};
|
||||
use crate::wutil::dir_iter::DirIter;
|
||||
use crate::wutil::fish_wcstoi;
|
||||
use crate::wutil::{normalize_path, waccess, wstat};
|
||||
use crate::wutil::{wbasename, wdirname};
|
||||
use libc::{ENOENT, PATH_MAX, R_OK, W_OK};
|
||||
use crate::wcstringutil::string_prefixes_string;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::os::fd::RawFd;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::file_tester::IsFile;
|
||||
|
||||
impl HighlightSpec {
|
||||
pub fn new() -> Self {
|
||||
|
@ -664,230 +647,6 @@ fn color_string_internal(buffstr: &wstr, base_color: HighlightSpec, colors: &mut
|
|||
}
|
||||
}
|
||||
|
||||
/// Indicates whether the source range of the given node forms a valid path in the given
|
||||
/// working_directory.
|
||||
fn range_is_potential_path(
|
||||
src: &wstr,
|
||||
range: SourceRange,
|
||||
at_cursor: bool,
|
||||
ctx: &OperationContext,
|
||||
working_directory: &wstr,
|
||||
) -> bool {
|
||||
// Skip strings exceeding PATH_MAX. See #7837.
|
||||
// Note some paths may exceed PATH_MAX, but this is just for highlighting.
|
||||
if range.length() > (PATH_MAX as usize) {
|
||||
return false;
|
||||
}
|
||||
// Get the node source, unescape it, and then pass it to is_potential_path along with the
|
||||
// working directory (as a one element list).
|
||||
let mut result = false;
|
||||
if let Some(mut token) = unescape_string(
|
||||
&src[range.start()..range.end()],
|
||||
crate::common::UnescapeStringStyle::Script(UnescapeFlags::SPECIAL),
|
||||
) {
|
||||
// Big hack: is_potential_path expects a tilde, but unescape_string gives us HOME_DIRECTORY.
|
||||
// Put it back.
|
||||
if token.char_at(0) == HOME_DIRECTORY {
|
||||
token.as_char_slice_mut()[0] = '~';
|
||||
}
|
||||
|
||||
result = is_potential_path(
|
||||
&token,
|
||||
at_cursor,
|
||||
&[working_directory.to_owned()],
|
||||
ctx,
|
||||
PathFlags {
|
||||
expand_tilde: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Tests whether the specified string cpath is the prefix of anything we could cd to. directories is
|
||||
// a list of possible parent directories (typically either the working directory, or the cdpath).
|
||||
// This does I/O!
|
||||
//
|
||||
// This is used only internally to this file, and is exposed only for testing.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PathFlags {
|
||||
// The path must be to a directory.
|
||||
pub require_dir: bool,
|
||||
// Expand any leading tilde in the path.
|
||||
pub expand_tilde: bool,
|
||||
// Normalize directories before resolving, as "cd".
|
||||
pub for_cd: bool,
|
||||
}
|
||||
|
||||
/// Tests whether the specified string cpath is the prefix of anything we could cd to. directories
|
||||
/// is a list of possible parent directories (typically either the working directory, or the
|
||||
/// cdpath). This does I/O!
|
||||
///
|
||||
/// Hack: if out_suggested_cdpath is not NULL, it returns the autosuggestion for cd. This descends
|
||||
/// the deepest unique directory hierarchy.
|
||||
///
|
||||
/// We expect the path to already be unescaped.
|
||||
pub fn is_potential_path(
|
||||
potential_path_fragment: &wstr,
|
||||
at_cursor: bool,
|
||||
directories: &[WString],
|
||||
ctx: &OperationContext<'_>,
|
||||
flags: PathFlags,
|
||||
) -> bool {
|
||||
assert_is_background_thread();
|
||||
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let require_dir = flags.require_dir;
|
||||
let mut clean_potential_path_fragment = WString::new();
|
||||
let mut has_magic = false;
|
||||
|
||||
let mut path_with_magic = potential_path_fragment.to_owned();
|
||||
if flags.expand_tilde {
|
||||
expand_tilde(&mut path_with_magic, ctx.vars());
|
||||
}
|
||||
|
||||
for c in path_with_magic.chars() {
|
||||
match c {
|
||||
PROCESS_EXPAND_SELF
|
||||
| VARIABLE_EXPAND
|
||||
| VARIABLE_EXPAND_SINGLE
|
||||
| BRACE_BEGIN
|
||||
| BRACE_END
|
||||
| BRACE_SEP
|
||||
| ANY_CHAR
|
||||
| ANY_STRING
|
||||
| ANY_STRING_RECURSIVE => {
|
||||
has_magic = true;
|
||||
}
|
||||
INTERNAL_SEPARATOR => (),
|
||||
_ => clean_potential_path_fragment.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
if has_magic || clean_potential_path_fragment.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't test the same path multiple times, which can happen if the path is absolute and the
|
||||
// CDPATH contains multiple entries.
|
||||
let mut checked_paths = HashSet::new();
|
||||
|
||||
// Keep a cache of which paths / filesystems are case sensitive.
|
||||
let mut case_sensitivity_cache = CaseSensitivityCache::new();
|
||||
|
||||
for wd in directories {
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
let mut abs_path = path_apply_working_directory(&clean_potential_path_fragment, wd);
|
||||
let must_be_full_dir = abs_path.chars().next_back() == Some('/');
|
||||
if flags.for_cd {
|
||||
abs_path = normalize_path(&abs_path, /*allow_leading_double_slashes=*/ true);
|
||||
}
|
||||
|
||||
// Skip this if it's empty or we've already checked it.
|
||||
if abs_path.is_empty() || checked_paths.contains(&abs_path) {
|
||||
continue;
|
||||
}
|
||||
checked_paths.insert(abs_path.clone());
|
||||
|
||||
// If the user is still typing the argument, we want to highlight it if it's the prefix
|
||||
// of a valid path. This means we need to potentially walk all files in some directory.
|
||||
// There are two easy cases where we can skip this:
|
||||
// 1. If the argument ends with a slash, it must be a valid directory, no prefix.
|
||||
// 2. If the cursor is not at the argument, it means the user is definitely not typing it,
|
||||
// so we can skip the prefix-match.
|
||||
if must_be_full_dir || !at_cursor {
|
||||
if let Ok(md) = wstat(&abs_path) {
|
||||
if !at_cursor || md.file_type().is_dir() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We do not end with a slash; it does not have to be a directory.
|
||||
let dir_name = wdirname(&abs_path);
|
||||
let filename_fragment = wbasename(&abs_path);
|
||||
if dir_name == "/" && filename_fragment == "/" {
|
||||
// cd ///.... No autosuggestion.
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Ok(mut dir) = DirIter::new(dir_name) {
|
||||
// Check if we're case insensitive.
|
||||
let do_case_insensitive =
|
||||
fs_is_case_insensitive(dir_name, dir.fd(), &mut case_sensitivity_cache);
|
||||
|
||||
// We opened the dir_name; look for a string where the base name prefixes it.
|
||||
while let Some(entry) = dir.next() {
|
||||
let Ok(entry) = entry else { continue };
|
||||
if ctx.check_cancel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Maybe skip directories.
|
||||
if require_dir && !entry.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if string_prefixes_string(filename_fragment, &entry.name)
|
||||
|| (do_case_insensitive
|
||||
&& string_prefixes_string_case_insensitive(
|
||||
filename_fragment,
|
||||
&entry.name,
|
||||
))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Given a string, return whether it prefixes a path that we could cd into. Return that path in
|
||||
// out_path. Expects path to be unescaped.
|
||||
fn is_potential_cd_path(
|
||||
path: &wstr,
|
||||
at_cursor: bool,
|
||||
working_directory: &wstr,
|
||||
ctx: &OperationContext<'_>,
|
||||
mut flags: PathFlags,
|
||||
) -> bool {
|
||||
let mut directories = vec![];
|
||||
|
||||
if string_prefixes_string(L!("./"), path) {
|
||||
// Ignore the CDPATH in this case; just use the working directory.
|
||||
directories.push(working_directory.to_owned());
|
||||
} else {
|
||||
// Get the CDPATH.
|
||||
let cdpath = ctx.vars().get_unless_empty(L!("CDPATH"));
|
||||
let mut pathsv = match cdpath {
|
||||
None => vec![L!(".").to_owned()],
|
||||
Some(cdpath) => cdpath.as_list().to_vec(),
|
||||
};
|
||||
// The current $PWD is always valid.
|
||||
pathsv.push(L!(".").to_owned());
|
||||
|
||||
for mut next_path in pathsv {
|
||||
if next_path.is_empty() {
|
||||
next_path = L!(".").to_owned();
|
||||
}
|
||||
// Ensure that we use the working directory for relative cdpaths like ".".
|
||||
directories.push(path_apply_working_directory(&next_path, working_directory));
|
||||
}
|
||||
}
|
||||
|
||||
// Call is_potential_path with all of these directories.
|
||||
flags.require_dir = true;
|
||||
flags.for_cd = true;
|
||||
is_potential_path(path, at_cursor, &directories, ctx, flags)
|
||||
}
|
||||
|
||||
pub type ColorArray = Vec<HighlightSpec>;
|
||||
|
||||
/// Syntax highlighter helper.
|
||||
|
@ -902,6 +661,8 @@ struct Highlighter<'s> {
|
|||
io_ok: bool,
|
||||
// Working directory.
|
||||
working_directory: WString,
|
||||
// Our component for testing strings for being potential file paths.
|
||||
file_tester: FileTester<'s>,
|
||||
// The resulting colors.
|
||||
color_array: ColorArray,
|
||||
// A stack of variables that the current commandline probably defines. We mark redirections
|
||||
|
@ -918,12 +679,14 @@ impl<'s> Highlighter<'s> {
|
|||
working_directory: WString,
|
||||
can_do_io: bool,
|
||||
) -> Self {
|
||||
let file_tester = FileTester::new(working_directory.clone(), ctx);
|
||||
Self {
|
||||
buff,
|
||||
cursor,
|
||||
ctx,
|
||||
io_ok: can_do_io,
|
||||
working_directory,
|
||||
file_tester,
|
||||
color_array: vec![],
|
||||
pending_variables: vec![],
|
||||
done: false,
|
||||
|
@ -1123,56 +886,35 @@ impl<'s> Highlighter<'s> {
|
|||
if !self.io_still_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Underline every valid path.
|
||||
let mut is_valid_path = false;
|
||||
let at_cursor = self
|
||||
let source_range = arg.source_range();
|
||||
let is_prefix = self
|
||||
.cursor
|
||||
.is_some_and(|c| arg.source_range().contains_inclusive(c));
|
||||
if cmd_is_cd {
|
||||
// Mark this as an error if it's not 'help' and not a valid cd path.
|
||||
let mut param = arg.source(self.buff).to_owned();
|
||||
if expand_one(&mut param, ExpandFlags::FAIL_ON_CMDSUBST, self.ctx, None) {
|
||||
let is_help = string_prefixes_string(¶m, L!("--help"))
|
||||
|| string_prefixes_string(¶m, L!("-h"));
|
||||
if !is_help {
|
||||
is_valid_path = is_potential_cd_path(
|
||||
¶m,
|
||||
at_cursor,
|
||||
&self.working_directory,
|
||||
self.ctx,
|
||||
PathFlags {
|
||||
expand_tilde: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if !is_valid_path {
|
||||
self.color_node(
|
||||
arg.as_node(),
|
||||
HighlightSpec::with_fg(HighlightRole::error),
|
||||
);
|
||||
}
|
||||
.map_or(false, |c| source_range.contains_inclusive(c));
|
||||
let token = arg.source(self.buff).to_owned();
|
||||
let test_result = if cmd_is_cd {
|
||||
self.file_tester.test_cd_path(&token, is_prefix)
|
||||
} else {
|
||||
let is_path = self.file_tester.test_path(&token, is_prefix);
|
||||
Ok(IsFile(is_path))
|
||||
};
|
||||
match test_result {
|
||||
Ok(IsFile(false)) => (),
|
||||
Ok(IsFile(true)) => {
|
||||
for i in source_range.as_usize() {
|
||||
self.color_array[i].valid_path = true;
|
||||
}
|
||||
}
|
||||
} else if range_is_potential_path(
|
||||
self.buff,
|
||||
arg.range().unwrap(),
|
||||
at_cursor,
|
||||
self.ctx,
|
||||
&self.working_directory,
|
||||
) {
|
||||
is_valid_path = true;
|
||||
}
|
||||
if is_valid_path {
|
||||
for i in arg.range().unwrap().start()..arg.range().unwrap().end() {
|
||||
self.color_array[i].valid_path = true;
|
||||
}
|
||||
Err(..) => self.color_node(arg.as_node(), HighlightSpec::with_fg(HighlightRole::error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_redirection(&mut self, redir: &Redirection) {
|
||||
// like 2>
|
||||
let oper = PipeOrRedir::try_from(redir.oper.source(self.buff))
|
||||
.expect("Should have successfully parsed a pipe_or_redir_t since it was in our ast");
|
||||
let mut target = redir.target.source(self.buff).to_owned(); // like &1 or file path
|
||||
let target = redir.target.source(self.buff).to_owned(); // like &1 or file path
|
||||
|
||||
// Color the > part.
|
||||
// It may have parsed successfully yet still be invalid (e.g. 9999999999999>&1)
|
||||
|
@ -1196,107 +938,30 @@ impl<'s> Highlighter<'s> {
|
|||
// even though it's a command redirection, and don't try to do any other validation.
|
||||
if has_cmdsub(&target) {
|
||||
self.color_as_argument(redir.target.leaf_as_node(), true);
|
||||
} else {
|
||||
// No command substitution, so we can highlight the target file or fd. For example,
|
||||
// disallow redirections into a non-existent directory.
|
||||
let target_is_valid;
|
||||
if !self.io_still_ok() {
|
||||
// I/O is disallowed, so we don't have much hope of catching anything but gross
|
||||
// errors. Assume it's valid.
|
||||
target_is_valid = true;
|
||||
} else if contains_pending_variable(&self.pending_variables, &target) {
|
||||
target_is_valid = true;
|
||||
} else if !expand_one(&mut target, ExpandFlags::FAIL_ON_CMDSUBST, self.ctx, None) {
|
||||
// Could not be expanded.
|
||||
target_is_valid = false;
|
||||
} else {
|
||||
// Ok, we successfully expanded our target. Now verify that it works with this
|
||||
// redirection. We will probably need it as a path (but not in the case of fd
|
||||
// redirections). Note that the target is now unescaped.
|
||||
let target_path = path_apply_working_directory(&target, &self.working_directory);
|
||||
match oper.mode {
|
||||
RedirectionMode::fd => {
|
||||
if target == "-" {
|
||||
target_is_valid = true;
|
||||
} else {
|
||||
target_is_valid = match fish_wcstoi(&target) {
|
||||
Ok(fd) => fd >= 0,
|
||||
Err(_) => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
RedirectionMode::input | RedirectionMode::try_input => {
|
||||
// Input redirections must have a readable non-directory.
|
||||
target_is_valid = waccess(&target_path, R_OK) == 0
|
||||
&& match wstat(&target_path) {
|
||||
Ok(md) => !md.file_type().is_dir(),
|
||||
Err(_) => false,
|
||||
};
|
||||
}
|
||||
RedirectionMode::overwrite
|
||||
| RedirectionMode::append
|
||||
| RedirectionMode::noclob => {
|
||||
// Test whether the file exists, and whether it's writable (possibly after
|
||||
// creating it). access() returns failure if the file does not exist.
|
||||
let file_exists;
|
||||
let file_is_writable;
|
||||
|
||||
if string_suffixes_string(L!("/"), &target) {
|
||||
// Redirections to things that are directories is definitely not
|
||||
// allowed.
|
||||
file_exists = false;
|
||||
file_is_writable = false;
|
||||
} else {
|
||||
match wstat(&target_path) {
|
||||
Ok(md) => {
|
||||
// No err. We can write to it if it's not a directory and we have
|
||||
// permission.
|
||||
file_exists = true;
|
||||
file_is_writable = !md.file_type().is_dir()
|
||||
&& waccess(&target_path, W_OK) == 0;
|
||||
}
|
||||
Err(err) => {
|
||||
if err.raw_os_error() == Some(ENOENT) {
|
||||
// File does not exist. Check if its parent directory is writable.
|
||||
let mut parent = wdirname(&target_path).to_owned();
|
||||
|
||||
// Ensure that the parent ends with the path separator. This will ensure
|
||||
// that we get an error if the parent directory is not really a
|
||||
// directory.
|
||||
if !string_suffixes_string(L!("/"), &parent) {
|
||||
parent.push('/');
|
||||
}
|
||||
|
||||
// Now the file is considered writable if the parent directory is
|
||||
// writable.
|
||||
file_exists = false;
|
||||
file_is_writable = waccess(&parent, W_OK) == 0;
|
||||
} else {
|
||||
// Other errors we treat as not writable. This includes things like
|
||||
// ENOTDIR.
|
||||
file_exists = false;
|
||||
file_is_writable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOCLOB means that we must not overwrite files that exist.
|
||||
target_is_valid = file_is_writable
|
||||
&& !(file_exists && oper.mode == RedirectionMode::noclob);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.color_node(
|
||||
redir.target.leaf_as_node(),
|
||||
HighlightSpec::with_fg(if target_is_valid {
|
||||
HighlightRole::redirection
|
||||
} else {
|
||||
HighlightRole::error
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// No command substitution, so we can highlight the target file or fd. For example,
|
||||
// disallow redirections into a non-existent directory.
|
||||
let target_is_valid = if !self.io_still_ok() {
|
||||
// I/O is disallowed, so we don't have much hope of catching anything but gross
|
||||
// errors. Assume it's valid.
|
||||
true
|
||||
} else if contains_pending_variable(&self.pending_variables, &target) {
|
||||
true
|
||||
} else {
|
||||
// Validate the redirection target..
|
||||
self.file_tester.test_redirection_target(&target, oper.mode)
|
||||
};
|
||||
self.color_node(
|
||||
redir.target.leaf_as_node(),
|
||||
HighlightSpec::with_fg(if target_is_valid {
|
||||
HighlightRole::redirection
|
||||
} else {
|
||||
HighlightRole::error
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn visit_variable_assignment(&mut self, varas: &VariableAssignment) {
|
||||
self.color_as_argument(varas, true);
|
||||
// Highlight the '=' in variable assignments as an operator.
|
||||
|
@ -1547,38 +1212,6 @@ fn get_fallback(role: HighlightRole) -> HighlightRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// Determine if the filesystem containing the given fd is case insensitive for lookups regardless
|
||||
/// of whether it preserves the case when saving a pathname.
|
||||
///
|
||||
/// Returns:
|
||||
/// false: the filesystem is not case insensitive
|
||||
/// true: the file system is case insensitive
|
||||
pub type CaseSensitivityCache = HashMap<WString, bool>;
|
||||
fn fs_is_case_insensitive(
|
||||
path: &wstr,
|
||||
fd: RawFd,
|
||||
case_sensitivity_cache: &mut CaseSensitivityCache,
|
||||
) -> bool {
|
||||
let mut result = false;
|
||||
if *_PC_CASE_SENSITIVE != 0 {
|
||||
// Try the cache first.
|
||||
match case_sensitivity_cache.entry(path.to_owned()) {
|
||||
Entry::Occupied(e) => {
|
||||
/* Use the cached value */
|
||||
result = *e.get();
|
||||
}
|
||||
Entry::Vacant(e) => {
|
||||
// Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case
|
||||
// sensitive, and a 0 value means case insensitive.
|
||||
let ret = unsafe { libc::fpathconf(fd, *_PC_CASE_SENSITIVE) };
|
||||
result = ret == 0;
|
||||
e.insert(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl Default for HighlightRole {
|
||||
fn default() -> Self {
|
||||
Self::normal
|
8
src/highlight/mod.rs
Normal file
8
src/highlight/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
pub mod file_tester;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod highlight;
|
||||
pub use file_tester::is_potential_path;
|
||||
pub use highlight::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
|
@ -5,7 +5,8 @@ use crate::tests::prelude::*;
|
|||
use crate::wchar::prelude::*;
|
||||
use crate::{
|
||||
env::EnvStack,
|
||||
highlight::{highlight_shell, is_potential_path, HighlightRole, HighlightSpec, PathFlags},
|
||||
highlight::file_tester::{is_potential_path, PathFlags},
|
||||
highlight::{highlight_shell, HighlightRole, HighlightSpec},
|
||||
operation_context::{OperationContext, EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT},
|
||||
};
|
||||
use libc::PATH_MAX;
|
||||
|
@ -646,3 +647,250 @@ fn test_highlighting() {
|
|||
("echo", fg(HighlightRole::error)),
|
||||
);
|
||||
}
|
||||
|
||||
pub use super::file_tester::{FileTester, IsErr, IsFile};
|
||||
mod file_tester_tests {
|
||||
use super::*;
|
||||
use crate::common::charptr2wcstring;
|
||||
use crate::redirection::RedirectionMode;
|
||||
use std::fs::{self, create_dir_all, File, Permissions};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
struct TempDir {
|
||||
basepath: WString,
|
||||
ctx: OperationContext<'static>,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> TempDir {
|
||||
let mut t1 = *b"/tmp/fish_file_tester_dir.XXXXXX\0";
|
||||
let basepath_narrow = unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) };
|
||||
assert!(!basepath_narrow.is_null(), "mkdtemp failed");
|
||||
let basepath: WString = charptr2wcstring(basepath_narrow);
|
||||
TempDir {
|
||||
basepath,
|
||||
ctx: OperationContext::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn filepath(&self, name: &str) -> String {
|
||||
let mut result = self.basepath.to_string();
|
||||
result.push('/');
|
||||
result.push_str(name);
|
||||
result
|
||||
}
|
||||
|
||||
fn file_tester(&self) -> FileTester<'_> {
|
||||
FileTester::new(self.basepath.clone(), &self.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(self.basepath.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ispath() {
|
||||
let temp = TempDir::new();
|
||||
let tester = temp.file_tester();
|
||||
|
||||
let file_path = temp.filepath("file.txt");
|
||||
File::create(&file_path).unwrap();
|
||||
|
||||
let result = tester.test_path(L!("file.txt"), false);
|
||||
assert!(result);
|
||||
|
||||
let result = tester.test_path(L!("file.txt"), true);
|
||||
assert!(result);
|
||||
|
||||
let result = tester.test_path(L!("fi"), false);
|
||||
assert!(!result);
|
||||
|
||||
let result = tester.test_path(L!("fi"), true);
|
||||
assert!(result);
|
||||
|
||||
let result = tester.test_path(L!("file.txt-more"), false);
|
||||
assert!(!result);
|
||||
|
||||
let result = tester.test_path(L!("file.txt-more"), true);
|
||||
assert!(!result);
|
||||
|
||||
let result = tester.test_path(L!("ffiledfk.txt"), false);
|
||||
assert!(!result);
|
||||
|
||||
let result = tester.test_path(L!("ffiledfk.txt"), true);
|
||||
assert!(!result);
|
||||
|
||||
// Directories are also files.
|
||||
let dir_path = temp.filepath("somedir");
|
||||
create_dir_all(&dir_path).unwrap();
|
||||
|
||||
let result = tester.test_path(L!("somedir"), false);
|
||||
assert!(result);
|
||||
|
||||
let result = tester.test_path(L!("somedir"), true);
|
||||
assert!(result);
|
||||
|
||||
let result = tester.test_path(L!("some"), false);
|
||||
assert!(!result);
|
||||
|
||||
let result = tester.test_path(L!("some"), true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iscdpath() {
|
||||
let temp = TempDir::new();
|
||||
let tester = temp.file_tester();
|
||||
|
||||
// Note cd (unlike file paths) should report IsErr for invalid cd paths,
|
||||
// rather than IsFile(false).
|
||||
|
||||
let dir_path = temp.filepath("somedir");
|
||||
create_dir_all(&dir_path).unwrap();
|
||||
|
||||
let result = tester.test_cd_path(L!("somedir"), false);
|
||||
assert_eq!(result, Ok(IsFile(true)));
|
||||
|
||||
let result = tester.test_cd_path(L!("somedir"), true);
|
||||
assert_eq!(result, Ok(IsFile(true)));
|
||||
|
||||
let result = tester.test_cd_path(L!("some"), false);
|
||||
assert_eq!(result, Err(IsErr));
|
||||
|
||||
let result = tester.test_cd_path(L!("some"), true);
|
||||
assert_eq!(result, Ok(IsFile(true)));
|
||||
|
||||
let result = tester.test_cd_path(L!("notdir"), false);
|
||||
assert_eq!(result, Err(IsErr));
|
||||
|
||||
let result = tester.test_cd_path(L!("notdir"), true);
|
||||
assert_eq!(result, Err(IsErr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirections() {
|
||||
// Note we use is_ok and is_err since we don't care about the IsFile part.
|
||||
let temp = TempDir::new();
|
||||
let tester = temp.file_tester();
|
||||
let file_path = temp.filepath("file.txt");
|
||||
File::create(&file_path).unwrap();
|
||||
|
||||
let dir_path = temp.filepath("somedir");
|
||||
create_dir_all(&dir_path).unwrap();
|
||||
|
||||
// Normal redirection.
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input);
|
||||
assert!(result);
|
||||
|
||||
// Can't redirect from a missing file
|
||||
let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::input);
|
||||
assert!(!result);
|
||||
let result =
|
||||
tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::input);
|
||||
assert!(!result);
|
||||
|
||||
// Can't redirect from a directory.
|
||||
let result = tester.test_redirection_target(L!("somedir"), RedirectionMode::input);
|
||||
assert!(!result);
|
||||
|
||||
// Can't redirect from an unreadable file.
|
||||
fs::set_permissions(&file_path, Permissions::from_mode(0o200)).unwrap();
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input);
|
||||
assert!(!result);
|
||||
fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap();
|
||||
|
||||
// try_input syntax highlighting reports an error even though the command will succeed.
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::try_input);
|
||||
assert!(result);
|
||||
let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::try_input);
|
||||
assert!(!result);
|
||||
let result =
|
||||
tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::try_input);
|
||||
assert!(!result);
|
||||
|
||||
// Test write redirections.
|
||||
// Overwrite an existing file.
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::overwrite);
|
||||
assert!(result);
|
||||
|
||||
// Append to an existing file.
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::append);
|
||||
assert!(result);
|
||||
|
||||
// Write to a missing file.
|
||||
let result = tester.test_redirection_target(L!("newfile.txt"), RedirectionMode::overwrite);
|
||||
assert!(result);
|
||||
|
||||
// No-clobber write to existing file should fail.
|
||||
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::noclob);
|
||||
assert!(!result);
|
||||
|
||||
// No-clobber write to missing file should succeed.
|
||||
let result = tester.test_redirection_target(L!("unique.txt"), RedirectionMode::noclob);
|
||||
assert!(result);
|
||||
|
||||
let write_modes = &[
|
||||
RedirectionMode::overwrite,
|
||||
RedirectionMode::append,
|
||||
RedirectionMode::noclob,
|
||||
];
|
||||
|
||||
// Can't write to a directory.
|
||||
for mode in write_modes {
|
||||
assert!(
|
||||
!tester.test_redirection_target(L!("somedir"), *mode),
|
||||
"Should not be able to write to a directory with mode {:?}",
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
// Can't write without write permissions.
|
||||
fs::set_permissions(&file_path, Permissions::from_mode(0o400)).unwrap(); // Read-only.
|
||||
for mode in write_modes {
|
||||
assert!(
|
||||
!tester.test_redirection_target(L!("file.txt"), *mode),
|
||||
"Should not be able to write to a read-only file with mode {:?}",
|
||||
mode
|
||||
);
|
||||
}
|
||||
fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap(); // Restore permissions.
|
||||
|
||||
// Writing into a directory without write permissions (loop through all modes).
|
||||
fs::set_permissions(&dir_path, Permissions::from_mode(0o500)).unwrap(); // Read and execute, no write.
|
||||
for mode in write_modes {
|
||||
assert!(
|
||||
!tester.test_redirection_target(L!("somedir/newfile.txt"), *mode),
|
||||
"Should not be able to create/write in a read-only directory with mode {:?}",
|
||||
mode
|
||||
);
|
||||
}
|
||||
fs::set_permissions(&dir_path, Permissions::from_mode(0o700)).unwrap(); // Restore permissions.
|
||||
|
||||
// Test fd redirections.
|
||||
assert!(tester.test_redirection_target(L!("-"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("0"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("1"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("2"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("3"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("500"), RedirectionMode::fd));
|
||||
|
||||
// We are base 10, despite the leading 0.
|
||||
assert!(tester.test_redirection_target(L!("000"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("01"), RedirectionMode::fd));
|
||||
assert!(tester.test_redirection_target(L!("07"), RedirectionMode::fd));
|
||||
|
||||
// Invalid fd redirections.
|
||||
assert!(!tester.test_redirection_target(L!("0x2"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("0x3F"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("0F"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("-1"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("-0009"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("--"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("derp"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("123boo"), RedirectionMode::fd));
|
||||
assert!(!tester.test_redirection_target(L!("18446744073709551616"), RedirectionMode::fd));
|
||||
}
|
||||
}
|
|
@ -44,6 +44,21 @@ pub struct SourceRange {
|
|||
pub length: u32,
|
||||
}
|
||||
|
||||
impl Default for SourceRange {
|
||||
fn default() -> Self {
|
||||
SourceRange {
|
||||
start: 0,
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceRange {
|
||||
pub fn as_usize(&self) -> std::ops::Range<usize> {
|
||||
(*self).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ParseTokenType {
|
||||
invalid = 1,
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::wutil::fish_wcstoi;
|
|||
use nix::fcntl::OFlag;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum RedirectionMode {
|
||||
overwrite, // normal redirection: > file.txt
|
||||
append, // appending redirection: >> file.txt
|
||||
|
|
|
@ -8,7 +8,6 @@ mod env;
|
|||
mod env_universal_common;
|
||||
mod expand;
|
||||
mod fd_monitor;
|
||||
mod highlight;
|
||||
mod history;
|
||||
mod input;
|
||||
mod input_common;
|
||||
|
|
Loading…
Reference in a new issue