mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
fix: escape control characters in filenames
This commit is contained in:
parent
c6e03e91d2
commit
180b94fb94
14 changed files with 207 additions and 74 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{mem, path::MAIN_SEPARATOR_STR};
|
use std::mem;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}};
|
use yazi_fs::{CWD, path::clean_url, provider::{DirReader, FileHolder}};
|
||||||
use yazi_macro::{act, render, succ};
|
use yazi_macro::{act, render, succ};
|
||||||
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
|
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
|
||||||
use yazi_proxy::CmpProxy;
|
use yazi_proxy::CmpProxy;
|
||||||
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
|
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
|
||||||
use yazi_vfs::provider;
|
use yazi_vfs::provider;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
@ -74,24 +74,25 @@ impl Trigger {
|
||||||
return None; // We don't autocomplete a `~`, but `~/`
|
return None; // We don't autocomplete a `~`, but `~/`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cwd = CWD.load();
|
||||||
|
let abs = if !path.is_absolute() && cwd.scheme().covariant(&scheme) {
|
||||||
|
cwd.loc().try_join(&path).ok()?.into()
|
||||||
|
} else {
|
||||||
|
PathCow::from(&path)
|
||||||
|
};
|
||||||
|
|
||||||
let sep = if cfg!(windows) {
|
let sep = if cfg!(windows) {
|
||||||
AnyAsciiChar::new(b"/\\").unwrap()
|
AnyAsciiChar::new(b"/\\").unwrap()
|
||||||
} else {
|
} else {
|
||||||
AnyAsciiChar::new(b"/").unwrap()
|
AnyAsciiChar::new(b"/").unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(match path.rsplit_pred(sep) {
|
let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c);
|
||||||
Some((p, c)) if p.is_empty() => {
|
let parent =
|
||||||
let root = PathDyn::with_str(scheme.kind(), MAIN_SEPARATOR_STR);
|
PathDyn::with(scheme.kind(), abs.encoded_bytes().strip_suffix(child.encoded_bytes())?)
|
||||||
(UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into())
|
.ok()?;
|
||||||
}
|
|
||||||
Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.into()),
|
Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child.into()))
|
||||||
None if CWD.load().scheme().covariant(&scheme) => (CWD.load().as_ref().clone(), path.into()),
|
|
||||||
None => {
|
|
||||||
let empty = PathDyn::with_str(scheme.kind(), "");
|
|
||||||
(UrlCow::try_from((scheme, empty)).ok()?.into_owned(), path.into())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,11 +131,11 @@ mod tests {
|
||||||
compare("/foo/bar", "/foo/", "bar");
|
compare("/foo/bar", "/foo/", "bar");
|
||||||
compare("///foo/bar", "/foo/", "bar");
|
compare("///foo/bar", "/foo/", "bar");
|
||||||
|
|
||||||
CWD.set(&"sftp://test/".parse::<UrlBuf>().unwrap(), || {});
|
CWD.set(&"sftp://test".parse::<UrlBuf>().unwrap(), || {});
|
||||||
compare("sftp://test/a", "sftp://test/", "a");
|
compare("sftp://test/a", "sftp://test/.", "a");
|
||||||
compare("sftp://test//a", "sftp://test:0//", "a");
|
compare("sftp://test//a", "sftp://test//", "a");
|
||||||
compare("sftp://test2/a", "sftp://test2/", "a");
|
compare("sftp://test2/a", "sftp://test2/.", "a");
|
||||||
compare("sftp://test2//a", "sftp://test2:0//", "a");
|
compare("sftp://test2//a", "sftp://test2//", "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use tokio::pin;
|
||||||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||||
use yazi_config::popup::InputCfg;
|
use yazi_config::popup::InputCfg;
|
||||||
use yazi_dds::Pubsub;
|
use yazi_dds::Pubsub;
|
||||||
use yazi_fs::{File, FilesOp, path::expand_url};
|
use yazi_fs::{File, FilesOp, path::{clean_url, expand_url}};
|
||||||
use yazi_macro::{act, err, render, succ};
|
use yazi_macro::{act, err, render, succ};
|
||||||
use yazi_parser::mgr::CdOpt;
|
use yazi_parser::mgr::CdOpt;
|
||||||
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
|
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
|
||||||
|
|
@ -71,6 +71,7 @@ impl Cd {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
|
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
|
||||||
let Ok(url) = provider::absolute(&url).await else { return };
|
let Ok(url) = provider::absolute(&url).await else { return };
|
||||||
|
let url = clean_url(url);
|
||||||
|
|
||||||
let Ok(file) = File::new(&url).await else { return };
|
let Ok(file) = File::new(&url).await else { return };
|
||||||
if file.is_dir() {
|
if file.is_dir() {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
|
||||||
|
|
||||||
macro_rules! push {
|
macro_rules! push {
|
||||||
($i:ident, $c:ident) => {{
|
($i:ident, $c:ident) => {{
|
||||||
out.push($c);
|
out.push(($i, $c));
|
||||||
if $i >= base {
|
if $i >= base {
|
||||||
uri_count += 1;
|
uri_count += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -31,12 +31,25 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! pop {
|
||||||
|
() => {{
|
||||||
|
if let Some((i, _)) = out.pop() {
|
||||||
|
if i >= base {
|
||||||
|
uri_count -= 1;
|
||||||
|
}
|
||||||
|
if i >= trail {
|
||||||
|
urn_count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
for (i, c) in path.components().enumerate() {
|
for (i, c) in path.components().enumerate() {
|
||||||
match c {
|
match c {
|
||||||
CurDir => {}
|
CurDir => {}
|
||||||
ParentDir => match out.last() {
|
ParentDir => match out.last().map(|(_, c)| c) {
|
||||||
Some(RootDir) => {}
|
Some(RootDir) => {}
|
||||||
Some(Normal(_)) => _ = out.pop(),
|
Some(Normal(_)) => pop!(),
|
||||||
None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c),
|
None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c),
|
||||||
},
|
},
|
||||||
c => push!(i, c),
|
c => push!(i, c),
|
||||||
|
|
@ -47,7 +60,8 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
|
||||||
let path = if out.is_empty() {
|
let path = if out.is_empty() {
|
||||||
PathBufDyn::with_str(kind, ".")
|
PathBufDyn::with_str(kind, ".")
|
||||||
} else {
|
} else {
|
||||||
PathBufDyn::from_components(kind, out).expect("components with same kind")
|
PathBufDyn::from_components(kind, out.into_iter().map(|(_, c)| c))
|
||||||
|
.expect("components with same kind")
|
||||||
};
|
};
|
||||||
|
|
||||||
(path, uri_count, urn_count)
|
(path, uri_count, urn_count)
|
||||||
|
|
@ -70,10 +84,11 @@ mod tests {
|
||||||
("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"),
|
("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"),
|
||||||
("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"),
|
("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"),
|
||||||
("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"),
|
("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"),
|
||||||
("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:3:2//foo/bar"),
|
("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"),
|
||||||
("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive://:1:1//tmp/test.zip"),
|
("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive:////tmp/test.zip"),
|
||||||
("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:2:1//tmp/test.zip"),
|
("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:1//tmp/test.zip"),
|
||||||
("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"),
|
("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"),
|
||||||
|
("sftp://test//root/.config/yazi/../../Downloads", "sftp://test//root/Downloads"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (input, expected) in cases {
|
for (input, expected) in cases {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function Entity:prefix()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Entity:highlights()
|
function Entity:highlights()
|
||||||
local name = self._file.name:gsub("\r", "?", 1)
|
local name = ui.printable(self._file.name)
|
||||||
local highlights = self._file:highlights()
|
local highlights = self._file:highlights()
|
||||||
if not highlights or #highlights == 0 then
|
if not highlights or #highlights == 0 then
|
||||||
return name
|
return name
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ function Status:name()
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
|
|
||||||
return " " .. h.name:gsub("\r", "?", 1)
|
return " " .. ui.printable(h.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status:perm()
|
function Status:perm()
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value};
|
use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span}};
|
use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span}};
|
||||||
use yazi_config::LAYOUT;
|
use yazi_config::LAYOUT;
|
||||||
use yazi_proxy::{AppProxy, HIDER};
|
use yazi_proxy::{AppProxy, HIDER};
|
||||||
|
use yazi_shared::replace_to_printable;
|
||||||
|
|
||||||
pub fn compose() -> Composer<ComposerGet, ComposerSet> {
|
pub fn compose() -> Composer<ComposerGet, ComposerSet> {
|
||||||
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
|
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
|
||||||
match key {
|
match key {
|
||||||
b"area" => area(lua)?,
|
b"area" => area(lua)?,
|
||||||
b"hide" => hide(lua)?,
|
b"hide" => hide(lua)?,
|
||||||
b"width" => width(lua)?,
|
b"printable" => printable(lua)?,
|
||||||
b"redraw" => redraw(lua)?,
|
b"redraw" => redraw(lua)?,
|
||||||
b"render" => render(lua)?,
|
b"render" => render(lua)?,
|
||||||
b"truncate" => truncate(lua)?,
|
b"truncate" => truncate(lua)?,
|
||||||
|
b"width" => width(lua)?,
|
||||||
_ => return Ok(Value::Nil),
|
_ => return Ok(Value::Nil),
|
||||||
}
|
}
|
||||||
.into_lua(lua)
|
.into_lua(lua)
|
||||||
|
|
@ -54,28 +58,12 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result<Value> {
|
||||||
f.into_lua(lua)
|
f.into_lua(lua)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn width(lua: &Lua) -> mlua::Result<Value> {
|
pub(super) fn printable(lua: &Lua) -> mlua::Result<Value> {
|
||||||
let f = lua.create_function(|_, v: Value| match v {
|
let f = lua.create_function(|lua, s: mlua::String| {
|
||||||
Value::String(s) => {
|
match replace_to_printable(&*s.as_bytes(), false, 1, true) {
|
||||||
let (mut acc, b) = (0, s.as_bytes());
|
Cow::Borrowed(_) => s.into_lua(lua),
|
||||||
for c in b.utf8_chunks() {
|
Cow::Owned(new) => new.into_lua(lua),
|
||||||
acc += c.valid().width();
|
|
||||||
if !c.invalid().is_empty() {
|
|
||||||
acc += 1;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(acc)
|
|
||||||
}
|
|
||||||
Value::UserData(ud) => {
|
|
||||||
if let Ok(line) = ud.borrow::<Line>() {
|
|
||||||
Ok(line.width())
|
|
||||||
} else if let Ok(span) = ud.borrow::<Span>() {
|
|
||||||
Ok(span.width())
|
|
||||||
} else {
|
|
||||||
Err("expected a string, Line, or Span".into_lua_err())?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err("expected a string, Line, or Span".into_lua_err())?,
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
f.into_lua(lua)
|
f.into_lua(lua)
|
||||||
|
|
@ -176,6 +164,33 @@ pub(super) fn truncate(lua: &Lua) -> mlua::Result<Value> {
|
||||||
f.into_lua(lua)
|
f.into_lua(lua)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn width(lua: &Lua) -> mlua::Result<Value> {
|
||||||
|
let f = lua.create_function(|_, v: Value| match v {
|
||||||
|
Value::String(s) => {
|
||||||
|
let (mut acc, b) = (0, s.as_bytes());
|
||||||
|
for c in b.utf8_chunks() {
|
||||||
|
acc += c.valid().width();
|
||||||
|
if !c.invalid().is_empty() {
|
||||||
|
acc += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(acc)
|
||||||
|
}
|
||||||
|
Value::UserData(ud) => {
|
||||||
|
if let Ok(line) = ud.borrow::<Line>() {
|
||||||
|
Ok(line.width())
|
||||||
|
} else if let Ok(span) = ud.borrow::<Span>() {
|
||||||
|
Ok(span.width())
|
||||||
|
} else {
|
||||||
|
Err("expected a string, Line, or Span".into_lua_err())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err("expected a string, Line, or Span".into_lua_err())?,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
f.into_lua(lua)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mlua::{Lua, chunk};
|
use mlua::{Lua, chunk};
|
||||||
|
|
|
||||||
14
yazi-plugin/src/external/highlighter.rs
vendored
14
yazi-plugin/src/external/highlighter.rs
vendored
|
|
@ -6,7 +6,7 @@ use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Th
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader};
|
||||||
use yazi_config::{THEME, YAZI, preview::PreviewWrap};
|
use yazi_config::{THEME, YAZI, preview::PreviewWrap};
|
||||||
use yazi_fs::provider::{Provider, local::Local};
|
use yazi_fs::provider::{Provider, local::Local};
|
||||||
use yazi_shared::{Ids, errors::PeekError, replace_to_printable};
|
use yazi_shared::{Ids, errors::PeekError, push_printable_char};
|
||||||
|
|
||||||
static INCR: Ids = Ids::new();
|
static INCR: Ids = Ids::new();
|
||||||
static SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new();
|
static SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new();
|
||||||
|
|
@ -95,7 +95,7 @@ impl Highlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(if plain {
|
Ok(if plain {
|
||||||
Text::from(replace_to_printable(&after, YAZI.preview.tab_size))
|
Text::from(Self::merge_highlight_lines(&after, YAZI.preview.tab_size))
|
||||||
} else {
|
} else {
|
||||||
Self::highlight_with(before, after, syntax.unwrap()).await?
|
Self::highlight_with(before, after, syntax.unwrap()).await?
|
||||||
})
|
})
|
||||||
|
|
@ -203,6 +203,16 @@ impl Highlighter {
|
||||||
*b = b'\n';
|
*b = b'\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_highlight_lines(s: &[String], tab_size: u8) -> String {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.reserve_exact(s.iter().map(|s| s.len()).sum::<usize>() | 15);
|
||||||
|
|
||||||
|
for &b in s.iter().flat_map(|s| s.as_bytes()) {
|
||||||
|
push_printable_char(&mut buf, b, true, tab_size, false);
|
||||||
|
}
|
||||||
|
unsafe { String::from_utf8_unchecked(buf) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
|
|
|
||||||
|
|
@ -94,26 +94,48 @@ pub fn replace_vec_cow<'a>(v: &'a [u8], from: &[u8], to: &[u8]) -> Cow<'a, [u8]>
|
||||||
Cow::Owned(out)
|
Cow::Owned(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_to_printable(s: &[String], tab_size: u8) -> String {
|
pub fn replace_to_printable(b: &[u8], lf: bool, tab_size: u8, replacement: bool) -> Cow<'_, [u8]> {
|
||||||
let mut buf = Vec::new();
|
// Fast path to skip over printable chars at the beginning of the string
|
||||||
buf.try_reserve_exact(s.iter().map(|s| s.len()).sum::<usize>() | 15).unwrap_or_else(|_| panic!());
|
let printable_len = b.iter().take_while(|&&c| !c.is_ascii_control()).count();
|
||||||
|
if printable_len >= b.len() {
|
||||||
|
return Cow::Borrowed(b);
|
||||||
|
}
|
||||||
|
|
||||||
for &b in s.iter().flat_map(|s| s.as_bytes()) {
|
let (printable, rest) = b.split_at(printable_len);
|
||||||
match b {
|
|
||||||
b'\n' => buf.push(b'\n'),
|
let mut out = Vec::new();
|
||||||
|
out.reserve_exact(b.len() | 15);
|
||||||
|
out.extend_from_slice(printable);
|
||||||
|
|
||||||
|
for &c in rest {
|
||||||
|
push_printable_char(&mut out, c, lf, tab_size, replacement);
|
||||||
|
}
|
||||||
|
Cow::Owned(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn push_printable_char(buf: &mut Vec<u8>, c: u8, lf: bool, tab_size: u8, replacement: bool) {
|
||||||
|
match c {
|
||||||
|
b'\n' if lf => buf.push(b'\n'),
|
||||||
b'\t' => {
|
b'\t' => {
|
||||||
buf.extend((0..tab_size).map(|_| b' '));
|
buf.extend((0..tab_size).map(|_| b' '));
|
||||||
}
|
}
|
||||||
b'\0'..=b'\x1F' => {
|
b'\0'..=b'\x1F' => {
|
||||||
|
if replacement {
|
||||||
|
buf.extend_from_slice(&[0xef, 0xbf, 0xbd]);
|
||||||
|
} else {
|
||||||
buf.push(b'^');
|
buf.push(b'^');
|
||||||
buf.push(b + b'@');
|
buf.push(c + b'@');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
0x7f => {
|
0x7f => {
|
||||||
|
if replacement {
|
||||||
|
buf.extend_from_slice(&[0xef, 0xbf, 0xbd]);
|
||||||
|
} else {
|
||||||
buf.push(b'^');
|
buf.push(b'^');
|
||||||
buf.push(b'?');
|
buf.push(b'?');
|
||||||
}
|
}
|
||||||
_ => buf.push(b),
|
}
|
||||||
|
_ => buf.push(c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unsafe { String::from_utf8_unchecked(buf) }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,22 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path {
|
||||||
where
|
where
|
||||||
T: AsStrandView<'a, Self::Strand<'a>>,
|
T: AsStrandView<'a, Self::Strand<'a>>,
|
||||||
{
|
{
|
||||||
self.strip_prefix(base.as_strand_view()).ok()
|
use std::path::is_separator;
|
||||||
|
|
||||||
|
let p = self.strip_prefix(base.as_strand_view()).ok()?;
|
||||||
|
let mut b = p.as_encoded_bytes();
|
||||||
|
|
||||||
|
if b.last().is_none_or(|&c| !is_separator(c as char)) || p.parent().is_none() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let [head @ .., last] = b
|
||||||
|
&& is_separator(*last as char)
|
||||||
|
{
|
||||||
|
b = head;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(unsafe { Self::from_encoded_bytes_unchecked(b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_path_buf(self) -> Self::Owned { self.to_path_buf() }
|
fn to_path_buf(self) -> Self::Owned { self.to_path_buf() }
|
||||||
|
|
@ -139,7 +154,18 @@ impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath {
|
||||||
where
|
where
|
||||||
T: AsStrandView<'a, Self::Strand<'a>>,
|
T: AsStrandView<'a, Self::Strand<'a>>,
|
||||||
{
|
{
|
||||||
self.strip_prefix(base.as_strand_view()).ok()
|
let p = self.strip_prefix(base.as_strand_view()).ok()?;
|
||||||
|
let mut b = p.as_bytes();
|
||||||
|
|
||||||
|
if b.last().is_none_or(|&c| c != b'/') || p.parent().is_none() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let [head @ .., b'/'] = b {
|
||||||
|
b = head;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(typed_path::UnixPath::new(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_path_buf(self) -> Self::Owned { self.to_path_buf() }
|
fn to_path_buf(self) -> Self::Owned { self.to_path_buf() }
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ where
|
||||||
{
|
{
|
||||||
let path = path.as_path_view();
|
let path = path.as_path_view();
|
||||||
let Some(name) = path.file_name() else {
|
let Some(name) = path.file_name() else {
|
||||||
let uri = path.strip_prefix(P::empty()).unwrap().len();
|
let p = path.strip_prefix(P::empty()).unwrap();
|
||||||
return Self { inner: path, uri, urn: 0, _phantom: PhantomData };
|
return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData };
|
||||||
};
|
};
|
||||||
|
|
||||||
let name_len = name.len();
|
let name_len = name.len();
|
||||||
|
|
@ -244,3 +244,38 @@ where
|
||||||
loc
|
loc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_with() -> Result<()> {
|
||||||
|
let cases = [
|
||||||
|
// Relative paths
|
||||||
|
("tmp/test.zip/foo/bar", 3, 2, "test.zip/foo/bar", "foo/bar"),
|
||||||
|
("tmp/test.zip/foo/bar/", 3, 2, "test.zip/foo/bar", "foo/bar"),
|
||||||
|
// Absolute paths
|
||||||
|
("/tmp/test.zip/foo/bar", 3, 2, "test.zip/foo/bar", "foo/bar"),
|
||||||
|
("/tmp/test.zip/foo/bar/", 3, 2, "test.zip/foo/bar", "foo/bar"),
|
||||||
|
// Relative path with parent components
|
||||||
|
("tmp/test.zip/foo/bar/../..", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."),
|
||||||
|
("tmp/test.zip/foo/bar/../../", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."),
|
||||||
|
// Absolute path with parent components
|
||||||
|
("/tmp/test.zip/foo/bar/../..", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."),
|
||||||
|
("/tmp/test.zip/foo/bar/../../", 5, 4, "test.zip/foo/bar/../..", "foo/bar/../.."),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, uri, urn, expect_uri, expect_urn) in cases {
|
||||||
|
let loc = Loc::with(std::path::Path::new(path), uri, urn)?;
|
||||||
|
assert_eq!(loc.uri().to_str().unwrap(), expect_uri);
|
||||||
|
assert_eq!(loc.urn().to_str().unwrap(), expect_urn);
|
||||||
|
|
||||||
|
let loc = Loc::with(typed_path::UnixPath::new(path), uri, urn)?;
|
||||||
|
assert_eq!(loc.uri().to_str().unwrap(), expect_uri);
|
||||||
|
assert_eq!(loc.urn().to_str().unwrap(), expect_urn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ impl From<PathBufDyn> for PathCow<'_> {
|
||||||
fn from(value: PathBufDyn) -> Self { Self::Owned(value) }
|
fn from(value: PathBufDyn) -> Self { Self::Owned(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a PathCow<'_>> for PathCow<'a> {
|
||||||
|
fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) }
|
||||||
|
}
|
||||||
|
|
||||||
impl From<PathCow<'_>> for PathBufDyn {
|
impl From<PathCow<'_>> for PathBufDyn {
|
||||||
fn from(value: PathCow<'_>) -> Self { value.into_owned() }
|
fn from(value: PathCow<'_>) -> Self { value.into_owned() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,10 @@ impl AsStrand for PathCow<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsStrand for &PathCow<'_> {
|
||||||
|
fn as_strand(&self) -> Strand<'_> { (**self).as_strand() }
|
||||||
|
}
|
||||||
|
|
||||||
impl AsStrand for Strand<'_> {
|
impl AsStrand for Strand<'_> {
|
||||||
fn as_strand(&self) -> Strand<'_> { *self }
|
fn as_strand(&self) -> Strand<'_> { *self }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ impl<'a> Strand<'a> {
|
||||||
|
|
||||||
let (skip, rest) = bytes.split_at(skip_len);
|
let (skip, rest) = bytes.split_at(skip_len);
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!());
|
out.reserve_exact(bytes.len());
|
||||||
out.extend(skip);
|
out.extend(skip);
|
||||||
|
|
||||||
for &b in rest {
|
for &b in rest {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ impl Transliterator for &[u8] {
|
||||||
// but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket
|
// but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket
|
||||||
// for short strings
|
// for short strings
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.try_reserve_exact(self.len() | 15).unwrap_or_else(|_| panic!());
|
out.reserve_exact(self.len() | 15);
|
||||||
out.push_str(unsafe { str::from_utf8_unchecked(ascii) });
|
out.push_str(unsafe { str::from_utf8_unchecked(ascii) });
|
||||||
|
|
||||||
for c in String::from_utf8_lossy(rest).chars() {
|
for c in String::from_utf8_lossy(rest).chars() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue