feat: add quirks path
This commit is contained in:
parent
7dabd46aa2
commit
b996be0702
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -3480,6 +3480,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quirks_path"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.35"
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/recorder"]
|
members = ["crates/quirks_path", "crates/recorder"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
11
crates/quirks_path/Cargo.toml
Normal file
11
crates/quirks_path/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "quirks_path"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nom = "7.1.3"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
thiserror = "1.0.57"
|
||||||
|
url = "2.5.0"
|
1687
crates/quirks_path/src/lib.rs
Normal file
1687
crates/quirks_path/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
97
crates/quirks_path/src/url.rs
Normal file
97
crates/quirks_path/src/url.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use std::{borrow::Cow, fmt::Write};
|
||||||
|
|
||||||
|
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
|
||||||
|
|
||||||
|
use crate::{windows::parse_drive, Component, Path, Prefix};
|
||||||
|
|
||||||
|
const URL_FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
||||||
|
const URL_PATH: &AsciiSet = &URL_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
|
||||||
|
const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%');
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum PathToUrlError {
|
||||||
|
#[error("Path not absolute: {path}")]
|
||||||
|
PathNotAbsoluteError { path: Cow<'static, str> },
|
||||||
|
#[error("Invalid UNC path")]
|
||||||
|
ParseUrlError(#[from] ::url::ParseError),
|
||||||
|
#[error("Path prefix can not be a url: {path}")]
|
||||||
|
UrlNotSupportedPrefix { path: Cow<'static, str> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn to_u32(i: usize) -> Result<u32, url::ParseError> {
|
||||||
|
if i <= ::std::u32::MAX as usize {
|
||||||
|
Ok(i as u32)
|
||||||
|
} else {
|
||||||
|
Err(url::ParseError::Overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn path_to_file_url_segments(
|
||||||
|
path: &Path,
|
||||||
|
serialization: &mut String,
|
||||||
|
) -> Result<(u32, Option<::url::Host<String>>), PathToUrlError> {
|
||||||
|
if !path.is_absolute() {
|
||||||
|
return Err(PathToUrlError::PathNotAbsoluteError {
|
||||||
|
path: Cow::Owned(path.as_str().to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut components = path.components();
|
||||||
|
|
||||||
|
let host_start = serialization.len() + 1;
|
||||||
|
let host_end;
|
||||||
|
let host_internal: Option<url::Host<String>>;
|
||||||
|
|
||||||
|
match components.next() {
|
||||||
|
Some(Component::Prefix(ref p)) => match p.kind() {
|
||||||
|
Prefix::Disk { drive } | Prefix::VerbatimDisk { drive } => {
|
||||||
|
host_end = to_u32(serialization.len()).unwrap();
|
||||||
|
host_internal = None;
|
||||||
|
serialization.push('/');
|
||||||
|
serialization.push(drive);
|
||||||
|
serialization.push(':');
|
||||||
|
}
|
||||||
|
Prefix::UNC { server, share } | Prefix::VerbatimUNC { server, share } => {
|
||||||
|
let host = url::Host::parse(server)?;
|
||||||
|
write!(serialization, "{}", host).unwrap();
|
||||||
|
host_end = to_u32(serialization.len()).unwrap();
|
||||||
|
host_internal = Some(host);
|
||||||
|
serialization.push('/');
|
||||||
|
serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(PathToUrlError::UrlNotSupportedPrefix {
|
||||||
|
path: Cow::Owned(path.as_str().to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(PathToUrlError::UrlNotSupportedPrefix {
|
||||||
|
path: Cow::Owned(path.as_str().to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path_only_has_prefix = true;
|
||||||
|
for component in components {
|
||||||
|
if matches!(component, Component::RootDir(..)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
path_only_has_prefix = false;
|
||||||
|
let component = component.as_str();
|
||||||
|
|
||||||
|
serialization.push('/');
|
||||||
|
serialization.extend(percent_encode(component.as_bytes(), URL_PATH_SEGMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A windows drive letter must end with a slash.
|
||||||
|
if serialization.len() > host_start
|
||||||
|
&& matches!(parse_drive(&serialization[host_start..]), Ok(..))
|
||||||
|
&& path_only_has_prefix
|
||||||
|
{
|
||||||
|
serialization.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((host_end, host_internal))
|
||||||
|
}
|
138
crates/quirks_path/src/windows.rs
Normal file
138
crates/quirks_path/src/windows.rs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
use nom::{
|
||||||
|
bytes::complete::tag,
|
||||||
|
character::complete::{self, satisfy},
|
||||||
|
combinator::peek,
|
||||||
|
error::{context, ContextError, Error, ErrorKind, ParseError},
|
||||||
|
sequence::pair,
|
||||||
|
AsChar, IResult, InputIter, InputTakeAtPosition,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Prefix;
|
||||||
|
|
||||||
|
fn non_slash(input: &str) -> IResult<&str, &str> {
|
||||||
|
input.split_at_position_complete(|item| item != '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_drive(path: &str) -> IResult<&str, char> {
|
||||||
|
context("drive", satisfy(char::is_alpha))(path).map(|a: (&str, char)| a)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_drive_exact(path: &str) -> IResult<&str, char> {
|
||||||
|
context("drive_exact", pair(parse_drive, complete::char(':')))(path)
|
||||||
|
.map(|(path, (drive, _))| (path, drive))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_windows_verbatim_sep(c: char) -> bool {
|
||||||
|
c == '\\'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_windows_sep(c: char) -> bool {
|
||||||
|
c == '\\' || c == '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_windows_next_component(path: &str, verbatim: bool) -> (&str, &str, &str) {
|
||||||
|
let separator = if verbatim {
|
||||||
|
is_windows_verbatim_sep
|
||||||
|
} else {
|
||||||
|
is_windows_sep
|
||||||
|
};
|
||||||
|
let p = path.as_bytes();
|
||||||
|
match p.position(|x| separator(x as char)) {
|
||||||
|
Some(separator_start) => {
|
||||||
|
let separator_end = separator_start + 1;
|
||||||
|
let component = &path[0..separator_start];
|
||||||
|
let path_with_sep = &path[separator_start..];
|
||||||
|
let path_without_sep = &path[separator_end..];
|
||||||
|
|
||||||
|
(component, path_with_sep, path_without_sep)
|
||||||
|
}
|
||||||
|
None => (path, "", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context_verify_error<'a>(input: &'a str, context: &'static str) -> nom::Err<Error<&'a str>> {
|
||||||
|
nom::Err::Error(Error::add_context(
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
Error::from_error_kind(input, ErrorKind::Verify),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_windows_path_prefix(raw_path: &str) -> IResult<&str, Prefix<'_>> {
|
||||||
|
if let Ok((path, _)) = tag(r"\\")(raw_path) as IResult<&str, &str> {
|
||||||
|
if let Ok((path, _)) = tag(r"?\")(path) as IResult<&str, &str> {
|
||||||
|
if let Ok((path, _)) = peek(non_slash)(path) as IResult<&str, &str> {
|
||||||
|
if let Ok((path, _)) = tag(r"UNC\")(path) as IResult<&str, &str> {
|
||||||
|
let (server, _, other) = parse_windows_next_component(path, true);
|
||||||
|
let (share, next_input, _) = parse_windows_next_component(other, true);
|
||||||
|
|
||||||
|
return Ok((next_input, Prefix::VerbatimUNC { server, share }));
|
||||||
|
} else if let Ok((path, drive)) = parse_drive_exact(path) {
|
||||||
|
return Ok((path, Prefix::VerbatimDisk { drive }));
|
||||||
|
} else {
|
||||||
|
let (prefix, next_input, _) = parse_windows_next_component(path, true);
|
||||||
|
return Ok((next_input, Prefix::Verbatim { prefix }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok((path, _)) = tag(r".\")(path) as IResult<&str, &str> {
|
||||||
|
let (prefix, next_input, _) = parse_windows_next_component(path, false);
|
||||||
|
return Ok((next_input, Prefix::DeviceNS { device: prefix }));
|
||||||
|
}
|
||||||
|
let (server, _, other) = parse_windows_next_component(path, false);
|
||||||
|
let (share, next_input, _) = parse_windows_next_component(other, false);
|
||||||
|
|
||||||
|
if !server.is_empty() && !share.is_empty() {
|
||||||
|
return Ok((next_input, Prefix::UNC { server, share }));
|
||||||
|
}
|
||||||
|
return Err(context_verify_error(raw_path, "windows path prefix"));
|
||||||
|
} else if let Ok((path, drive)) = parse_drive_exact(raw_path) {
|
||||||
|
return Ok((path, Prefix::Disk { drive }));
|
||||||
|
} else {
|
||||||
|
return Err(context_verify_error(raw_path, "windows path prefix"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_parse_windows_path_prefix() {
|
||||||
|
use super::*;
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"\\?\UNC\server\share\path"),
|
||||||
|
Ok((
|
||||||
|
r"\path",
|
||||||
|
Prefix::VerbatimUNC {
|
||||||
|
server: "server",
|
||||||
|
share: "share"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"\\?\C:\path"),
|
||||||
|
Ok((r"\path", Prefix::VerbatimDisk { drive: 'C' }))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"\\server\share\path"),
|
||||||
|
Ok((
|
||||||
|
r"\path",
|
||||||
|
Prefix::UNC {
|
||||||
|
server: "server",
|
||||||
|
share: "share"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"C:\path"),
|
||||||
|
Ok((r"\path", Prefix::Disk { drive: 'C' }))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"\\.\device\path"),
|
||||||
|
Ok((r"\path", Prefix::DeviceNS { device: "device" }))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_windows_path_prefix(r"\\?\abc\path"),
|
||||||
|
Ok((r"\path", Prefix::Verbatim { prefix: "abc" }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -457,11 +457,7 @@ pub mod tests {
|
|||||||
let docker = testcontainers::clients::Cli::default();
|
let docker = testcontainers::clients::Cli::default();
|
||||||
let image = create_qbit_testcontainer();
|
let image = create_qbit_testcontainer();
|
||||||
|
|
||||||
let container = docker.run(image);
|
let _container = docker.run(image);
|
||||||
|
|
||||||
let mut exec = ExecCommand::default();
|
|
||||||
|
|
||||||
container.exec(exec);
|
|
||||||
|
|
||||||
test_qbittorrent_downloader_impl().await;
|
test_qbittorrent_downloader_impl().await;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::path::PathBuf;
|
use std::{borrow::Cow, collections::VecDeque, path::PathBuf};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
pub use uni_path::{Path as VFSSubPath, PathBuf as VFSSubPathBuf};
|
pub use uni_path::{Path as VFSSubPath, PathBuf as VFSSubPathBuf};
|
||||||
@ -77,8 +77,8 @@ impl<'a> VFSPath<'a> {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct VFSPathBuf {
|
pub struct VFSPathBuf {
|
||||||
pub root: String,
|
root: String,
|
||||||
pub sub: VFSSubPathBuf,
|
sub: VFSSubPathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VFSPathBuf {
|
impl VFSPathBuf {
|
||||||
|
Loading…
Reference in New Issue
Block a user