use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
pub fn literal_path(prefix: &[u8], root: &Path, token: &OsStr) -> Option<Vec<u8>> {
let bytes: &[u8] = token.as_bytes();
if bytes.is_empty() || bytes[0] == b':' || bytes.ends_with(b"/") || bytes.iter().any(is_wildcard) {
return None;
}
let joined: Vec<u8> = if bytes[0] == b'/' {
let below: &Path = Path::new(token).strip_prefix(root).ok()?;
below.as_os_str().as_bytes().to_vec()
} else {
[prefix, bytes].concat()
};
let mut kept: Vec<&[u8]> = Vec::new();
for segment in joined.split(is_slash) {
match segment {
b"" | b"." => {}
b".." => {
kept.pop()?;
}
other => kept.push(other),
}
}
if kept.is_empty() {
return None;
}
return Some(kept.join(&b'/'));
}
fn is_wildcard(byte: &u8) -> bool {
return matches!(byte, b'*' | b'?' | b'[' | b'\\');
}
fn is_slash(byte: &u8) -> bool {
return *byte == b'/';
}
fn is_close_paren(byte: &u8) -> bool {
return *byte == b')';
}
fn is_comma(byte: &u8) -> bool {
return *byte == b',';
}
pub fn magic_name(prefix: &[u8], root: &Path, token: &OsStr) -> Option<(String, Vec<u8>)> {
let bytes: &[u8] = token.as_bytes();
if bytes.first() != Some(&b':') {
return None;
}
let mut top: bool = false;
let length: usize = if bytes.get(1) == Some(&b'(') {
let close: usize = bytes.iter().position(is_close_paren)?;
for word in bytes[2..close].split(is_comma) {
match word {
b"top" => top = true,
b"literal" | b"icase" | b"glob" | b"exclude" => {}
_ => return None,
}
}
close + 1
} else {
let mut at: usize = 1;
while at < bytes.len() && matches!(bytes[at], b'!' | b'^' | b'/') {
top |= bytes[at] == b'/';
at += 1;
}
if at == 1 {
return None;
}
if bytes.get(at) == Some(&b':') {
at += 1;
}
at
};
let path: Vec<u8> = literal_path(if top { b"" } else { prefix }, root, OsStr::from_bytes(&bytes[length..]))?;
return Some((String::from_utf8_lossy(&bytes[..length]).into_owned(), path));
}
pub fn magic_on_conflicted(others: &[OsString], prefix: &[u8], root: &Path, conflicted: &BTreeSet<Vec<u8>>) -> Option<Vec<(String, String)>> {
let mut found: Vec<(String, String)> = Vec::new();
for token in others {
let (magic, path) = magic_name(prefix, root, token.as_os_str())?;
if !conflicted.contains(&path) {
return None;
}
found.push((magic, String::from_utf8_lossy(&path).into_owned()));
}
return if found.is_empty() { None } else { Some(found) };
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SideDecision {
Allow(Vec<Vec<u8>>),
Refuse(Vec<OsString>),
}
pub fn decide_sides(tokens: &[OsString], prefix: &[u8], root: &Path, conflicted: &BTreeSet<Vec<u8>>) -> SideDecision {
let mut allowed: Vec<Vec<u8>> = Vec::new();
let mut others: Vec<OsString> = Vec::new();
for token in tokens {
match literal_path(prefix, root, token.as_os_str()) {
Some(path) if conflicted.contains(&path) => {
if !allowed.contains(&path) {
allowed.push(path);
}
}
_ => others.push(token.clone()),
}
}
if others.is_empty() {
return SideDecision::Allow(allowed);
}
return SideDecision::Refuse(others);
}