use alphanumeric_sort::compare_os_str;
use nu_protocol::ShellError;
use nu_protocol::Span;
use powierza_coefficient::powierża_coefficient;
use std::cmp::{Ord, Ordering};
use std::{
convert::AsRef,
ffi::{OsStr, OsString},
fs::DirEntry,
mem,
path::{Component, Path, PathBuf},
};
struct Finding {
file_name: OsString,
path: PathBuf,
congruence: Vec<Congruence>,
}
fn get_matching_children<'a, P>(
path: &'a P,
abbr: &'a Abbr,
parent_congruence: &'a [Congruence],
) -> impl Iterator<Item = Finding> + 'a
where
P: AsRef<Path>,
{
let filter_map_entry = move |entry: DirEntry| {
let file_type = entry.file_type().ok()?;
if file_type.is_dir() || file_type.is_symlink() {
let file_name: String = entry.file_name().into_string().ok()?;
if let Some(congruence) = abbr.compare(&file_name) {
let mut entry_congruence = parent_congruence.to_vec();
entry_congruence.insert(0, congruence);
return Some(Finding {
file_name: entry.file_name(),
congruence: entry_congruence,
path: entry.path(),
});
}
}
None
};
path.as_ref()
.read_dir()
.ok()
.map(|reader| {
reader
.filter_map(|entry| entry.ok())
.filter_map(filter_map_entry)
})
.into_iter()
.flatten()
}
pub fn query<P>(arg: &P, excluded: Option<PathBuf>, span: Span) -> Result<PathBuf, ShellError>
where
P: AsRef<Path>,
{
if arg.as_ref().is_dir() {
return Ok(arg.as_ref().into());
}
let (prefix, abbrs) = parse_arg(&arg)?;
let start_dir = match prefix {
Some(start_dir) => start_dir,
None => std::env::current_dir()?,
};
match abbrs.as_slice() {
[] => Ok(start_dir),
[first_abbr, abbrs @ ..] => {
let mut current_level =
get_matching_children(&start_dir, first_abbr, &[]).collect::<Vec<_>>();
let mut next_level = vec![];
for abbr in abbrs {
let children = current_level.iter().flat_map(|parent| {
get_matching_children(&parent.path, abbr, &parent.congruence)
});
next_level.clear();
next_level.extend(children);
mem::swap(&mut next_level, &mut current_level);
}
let cmp_findings = |finding_a: &Finding, finding_b: &Finding| {
finding_a
.congruence
.cmp(&finding_b.congruence)
.then(compare_os_str(&finding_a.file_name, &finding_b.file_name))
};
let found_path = match excluded {
Some(excluded) if current_level.len() > 1 => current_level
.into_iter()
.filter(|finding| finding.path != excluded)
.min_by(cmp_findings)
.map(|Finding { path, .. }| path),
_ => current_level
.into_iter()
.min_by(cmp_findings)
.map(|Finding { path, .. }| path),
};
found_path.ok_or(ShellError::NotADirectory(span))
}
}
}
fn parse_dots(component: &str) -> Option<usize> {
component
.chars()
.try_fold(
0,
|n_dots, c| if c == '.' { Some(n_dots + 1) } else { None },
)
.and_then(|n_dots| if n_dots > 1 { Some(n_dots - 1) } else { None })
}
fn extract_prefix<'a, P>(
arg: &'a P,
) -> Result<(Option<PathBuf>, impl Iterator<Item = Component<'a>> + 'a), ShellError>
where
P: AsRef<Path> + ?Sized + 'a,
{
use Component::*;
let mut components = arg.as_ref().components().peekable();
let mut prefix: Option<PathBuf> = None;
let mut push_to_prefix = |component: Component| match &mut prefix {
None => prefix = Some(PathBuf::from(&component)),
Some(prefix) => prefix.push(component),
};
let parse_dots_os = |component_os: &OsStr| {
component_os
.to_os_string()
.into_string()
.map_err(|_| ShellError::NonUnicodeInput)
.map(|component| parse_dots(&component))
};
while let Some(component) = components.peek() {
match component {
Prefix(_) | RootDir | CurDir | ParentDir => push_to_prefix(*component),
Normal(component_os) => {
if let Some(n_dots) = parse_dots_os(component_os)? {
(0..n_dots).for_each(|_| push_to_prefix(ParentDir));
} else {
break;
}
}
}
let _consumed = components.next();
}
Ok((prefix, components))
}
fn parse_abbrs<'a, I>(components: I) -> Result<Vec<Abbr>, ShellError>
where
I: Iterator<Item = Component<'a>> + 'a,
{
use Component::*;
let abbrs = components
.into_iter()
.map(|component| match component {
Prefix(_) | RootDir | CurDir | ParentDir => {
let component_string = component
.as_os_str()
.to_os_string()
.to_string_lossy()
.to_string();
Err(ShellError::UnexpectedAbbrComponent(component_string))
}
Normal(component_os) => component_os
.to_os_string()
.into_string()
.map_err(|_| ShellError::NonUnicodeInput)
.map(|string| Abbr::new_sanitized(&string)),
})
.collect::<Result<Vec<_>, _>>()?;
Ok(abbrs)
}
fn parse_arg<P>(arg: &P) -> Result<(Option<PathBuf>, Vec<Abbr>), ShellError>
where
P: AsRef<Path>,
{
let (prefix, suffix) = extract_prefix(arg)?;
let abbrs = parse_abbrs(suffix)?;
Ok((prefix, abbrs))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_extract_prefix() {
{
let (prefix, suffix) = extract_prefix("suf/fix").unwrap();
let suffix = suffix.collect::<PathBuf>();
assert_eq!(prefix, None);
assert_eq!(as_path(&suffix), as_path("suf/fix"));
}
{
let (prefix, suffix) = extract_prefix("./.././suf/fix").unwrap();
let suffix = suffix.collect::<PathBuf>();
assert_eq!(prefix.unwrap(), as_path("./.."));
assert_eq!(as_path(&suffix), as_path("suf/fix"));
}
{
let (prefix, suffix) = extract_prefix(".../.../suf/fix").unwrap();
let suffix = suffix.collect::<PathBuf>();
assert_eq!(prefix.unwrap(), as_path("../../../.."));
assert_eq!(as_path(&suffix), as_path("suf/fix"));
}
}
#[test]
fn test_parse_arg_invalid_unicode() {
#[cfg(unix)]
{
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let source = [0x66, 0x6f, 0x80, 0x6f];
let non_unicode_input = OsStr::from_bytes(&source[..]).to_os_string();
let result = parse_arg(&non_unicode_input);
assert!(result.is_err());
}
#[cfg(windows)]
{
use std::os::windows::prelude::*;
let source = [0x0066, 0x006f, 0xd800, 0x006f];
let os_string = OsString::from_wide(&source[..]);
let result = parse_arg(&os_string);
assert!(result.is_err());
}
}
#[test]
fn test_congruence_ordering() {
assert!(Complete < Prefix);
assert!(Complete < Subsequence(1));
assert!(Prefix < Subsequence(1));
assert!(Subsequence(1) < Subsequence(1000));
}
#[test]
fn test_order_paths() {
fn sort<'a>(paths: &'a [&'a str], abbr: &str) -> Vec<&'a str> {
let abbr = Abbr::new_sanitized(abbr);
let mut paths = paths.to_owned();
paths.sort_by_key(|path| abbr.compare(path).unwrap());
paths
}
let paths = vec!["playground", "plotka"];
assert_eq!(paths, sort(&paths, "pla"));
let paths = vec!["veccentric", "vehiccles"];
assert_eq!(paths, sort(&paths, "vecc"));
}
}
#[cfg(any(test, doc))]
pub fn as_path<P>(path: &P) -> &Path
where
P: AsRef<Path> + ?Sized,
{
path.as_ref()
}
#[derive(Debug, Clone)]
pub enum Abbr {
Wildcard,
Literal(String),
}
impl Abbr {
pub fn new_sanitized(abbr: &str) -> Self {
if abbr == "-" {
Self::Wildcard
} else {
Self::Literal(abbr.to_ascii_lowercase())
}
}
pub fn compare(&self, component: &str) -> Option<Congruence> {
let component = component.to_ascii_lowercase();
match self {
Self::Wildcard => Some(Congruence::Complete),
Self::Literal(literal) => {
if literal.is_empty() || component.is_empty() {
None
} else if *literal == component {
Some(Congruence::Complete)
} else if component.starts_with(literal) {
Some(Congruence::Prefix)
} else {
powierża_coefficient(literal, &component).map(Congruence::Subsequence)
}
}
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Congruence {
Complete,
Prefix,
Subsequence(u32),
}
use Congruence::*;
impl PartialOrd for Congruence {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(Ord::cmp(self, other))
}
}
impl Ord for Congruence {
fn cmp(&self, other: &Self) -> Ordering {
use Ordering::*;
match (self, other) {
(Complete, Complete) => Equal,
(Complete, Prefix) => Less,
(Complete, Subsequence(_)) => Less,
(Prefix, Complete) => Greater,
(Prefix, Prefix) => Equal,
(Prefix, Subsequence(_)) => Less,
(Subsequence(_), Complete) => Greater,
(Subsequence(_), Prefix) => Greater,
(Subsequence(dist_a), Subsequence(dist_b)) => dist_a.cmp(dist_b),
}
}
}