From dccc9b2c2a2317fa2d7a73c44fd99bb0dacd6360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 4 Dec 2025 16:13:39 +0800 Subject: [PATCH] fix: escape control characters in filenames (#3400) --- Cargo.lock | 267 ++++++++++++++--------- Cargo.toml | 4 +- yazi-actor/src/cmp/trigger.rs | 41 ++-- yazi-actor/src/mgr/cd.rs | 3 +- yazi-fs/src/path/clean.rs | 29 ++- yazi-plugin/preset/components/entity.lua | 10 +- yazi-plugin/preset/components/status.lua | 2 +- yazi-plugin/src/elements/elements.rs | 61 ++++-- yazi-plugin/src/external/highlighter.rs | 14 +- yazi-shared/src/chars.rs | 50 +++-- yazi-shared/src/loc/able.rs | 30 ++- yazi-shared/src/loc/loc.rs | 39 +++- yazi-shared/src/path/cow.rs | 4 + yazi-shared/src/strand/conversion.rs | 4 + yazi-shared/src/strand/strand.rs | 2 +- yazi-shared/src/translit/traits.rs | 2 +- 16 files changed, 374 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3267b6f7..4303d578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -283,7 +283,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -300,9 +300,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bcrypt-pbkdf" @@ -391,7 +391,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -400,7 +400,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -548,7 +548,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -694,9 +694,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -709,9 +709,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-models" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94950e87ea550d6d68f1993f3e7bebc8cb7235157bff84337d46195c3aa0b3f0" +checksum = "0940496e5c83c54f3b753d5317daec82e8edac71c33aaa1f666d76f518de2444" dependencies = [ "hax-lib", "pastey", @@ -837,7 +837,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -849,7 +849,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -1015,22 +1015,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -1131,7 +1132,7 @@ dependencies = [ "crypto-bigint", "digest", "ff", - "generic-array", + "generic-array 0.14.7", "group", "hkdf", "pem-rfc7468", @@ -1445,6 +1446,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "generic-array 0.14.7", + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1482,9 +1494,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -1759,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1901,15 +1913,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libcrux-intrinsics" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3b41dcbc21a5fb7efbbb5af7405b2e79c4bfe443924e90b13afc0080318d31" +checksum = "bc9ee7ef66569dd7516454fe26de4e401c0c62073929803486b96744594b9632" dependencies = [ "core-models", "hax-lib", @@ -1917,16 +1929,18 @@ dependencies = [ [[package]] name = "libcrux-ml-kem" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d368d3e8d6a74e277178d54921eca112a1e6b7837d7d8bc555091acb5d817f5" +checksum = "4bb6a88086bf11bd2ec90926c749c4a427f2e59841437dbdede8cde8a96334ab" dependencies = [ "hax-lib", "libcrux-intrinsics", "libcrux-platform", "libcrux-secrets", "libcrux-sha3", + "libcrux-traits", "rand 0.9.2", + "tls_codec", ] [[package]] @@ -1940,22 +1954,33 @@ dependencies = [ [[package]] name = "libcrux-secrets" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332737e629fe6ba7547f5c0f90559eac865d5dbecf98138ffae8f16ab8cbe33f" +checksum = "6e4dbbf6bc9f2bc0f20dc3bea3e5c99adff3bdccf6d2a40488963da69e2ec307" dependencies = [ "hax-lib", ] [[package]] name = "libcrux-sha3" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d95de4257eafdfaf3bffecadb615219b0ca920c553722b3646d32dde76c797" +checksum = "2400bec764d1c75b8a496d5747cffe32f1fb864a12577f0aca2f55a92021c962" dependencies = [ "hax-lib", "libcrux-intrinsics", "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adfd58e79d860f6b9e40e35127bfae9e5bd3ade33201d1347459011a2add034" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", ] [[package]] @@ -2013,9 +2038,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -2501,9 +2526,9 @@ dependencies = [ [[package]] name = "pageant" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb28bd89a207e5cad59072ac4b364b08459d05f90ccfbcdaa920a95857d94430" +checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" dependencies = [ "byteorder", "bytes", @@ -2511,9 +2536,11 @@ dependencies = [ "futures", "log", "rand 0.8.5", + "sha2", "thiserror 1.0.69", "tokio", - "windows 0.59.0", + "windows 0.62.2", + "windows-strings", ] [[package]] @@ -2566,7 +2593,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3139,12 +3166,11 @@ dependencies = [ [[package]] name = "russh" -version = "0.54.6" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ca8e9091b72afdc9979bddcd1988ad898fa7bc0b85c8da8c154c29d44319eb" +checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548" dependencies = [ "aes", - "base64ct", "bitflags 2.10.0", "block-padding", "byteorder", @@ -3161,7 +3187,7 @@ dependencies = [ "elliptic-curve", "enum_dispatch", "futures", - "generic-array", + "generic-array 1.3.5", "getrandom 0.2.16", "hex-literal", "hmac", @@ -3172,7 +3198,6 @@ dependencies = [ "log", "md5", "num-bigint", - "once_cell", "p256", "p384", "p521", @@ -3336,7 +3361,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -3772,6 +3797,27 @@ dependencies = [ "time-core", ] +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.48.0" @@ -4046,9 +4092,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -4290,12 +4336,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.59.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.5", + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -4310,19 +4367,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.3", - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.5", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4331,9 +4375,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link 0.2.1", + "windows-link", "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", ] [[package]] @@ -4347,17 +4402,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -4391,18 +4435,22 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -4412,31 +4460,13 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4445,7 +4475,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4481,7 +4511,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4506,7 +4536,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -4517,6 +4547,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5176,18 +5215,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -5199,6 +5238,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 5ab7628e..e0d1044b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ futures = "0.3.31" globset = "0.4.18" hashbrown = { version = "0.16.1", features = [ "serde" ] } indexmap = { version = "2.12.1", features = [ "serde" ] } -libc = "0.2.177" +libc = "0.2.178" lru = "0.16.2" mlua = { version = "0.11.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } objc2 = "0.6.3" @@ -44,7 +44,7 @@ percent-encoding = "2.3.2" rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.2" -russh = { version = "0.54.6", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.55.0", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.145" diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 07f192ff..91e18739 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,11 +1,11 @@ -use std::{mem, path::MAIN_SEPARATOR_STR}; +use std::mem; 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_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; 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 crate::{Actor, Ctx}; @@ -74,24 +74,25 @@ impl Trigger { 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) { AnyAsciiChar::new(b"/\\").unwrap() } else { AnyAsciiChar::new(b"/").unwrap() }; - Some(match path.rsplit_pred(sep) { - Some((p, c)) if p.is_empty() => { - let root = PathDyn::with_str(scheme.kind(), MAIN_SEPARATOR_STR); - (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) - } - Some((p, c)) => (expand_url(UrlCow::try_from((scheme, p)).ok()?), c.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()) - } - }) + let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c); + let parent = + PathDyn::with(scheme.kind(), abs.encoded_bytes().strip_suffix(child.encoded_bytes())?) + .ok()?; + + Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child.into())) } } @@ -130,11 +131,11 @@ mod tests { compare("/foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar"); - CWD.set(&"sftp://test/".parse::().unwrap(), || {}); - compare("sftp://test/a", "sftp://test/", "a"); - compare("sftp://test//a", "sftp://test:0//", "a"); - compare("sftp://test2/a", "sftp://test2/", "a"); - compare("sftp://test2//a", "sftp://test2:0//", "a"); + CWD.set(&"sftp://test".parse::().unwrap(), || {}); + compare("sftp://test/a", "sftp://test/.", "a"); + compare("sftp://test//a", "sftp://test//", "a"); + compare("sftp://test2/a", "sftp://test2/.", "a"); + compare("sftp://test2//a", "sftp://test2//", "a"); } #[cfg(windows)] diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 0f855dbc..1e0a6287 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; 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_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; @@ -71,6 +71,7 @@ impl Cd { Ok(s) => { let Ok(url) = UrlBuf::try_from(s).map(expand_url) 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 }; if file.is_dir() { diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index a5b9bc29..7fdac319 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -21,7 +21,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi macro_rules! push { ($i:ident, $c:ident) => {{ - out.push($c); + out.push(($i, $c)); if $i >= base { 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() { match c { CurDir => {} - ParentDir => match out.last() { + ParentDir => match out.last().map(|(_, c)| c) { Some(RootDir) => {} - Some(Normal(_)) => _ = out.pop(), + Some(Normal(_)) => pop!(), None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => 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() { PathBufDyn::with_str(kind, ".") } 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) @@ -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//test.zip/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://:4:4//tmp/test.zip/foo/bar/../../", "archive://:1:1//tmp/test.zip"), - ("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:2:1//tmp/test.zip"), + ("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), + ("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive:////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"), + ("sftp://test//root/.config/yazi/../../Downloads", "sftp://test//root/Downloads"), ]; for (input, expected) in cases { diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 7a287128..3f416737 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -31,22 +31,22 @@ function Entity:prefix() end function Entity:highlights() - local name = self._file.name:gsub("\r", "?", 1) + local name, p = self._file.name, ui.printable local highlights = self._file:highlights() if not highlights or #highlights == 0 then - return name + return p(name) end local spans, last = {}, 0 for _, h in ipairs(highlights) do if h[1] > last then - spans[#spans + 1] = name:sub(last + 1, h[1]) + spans[#spans + 1] = p(name:sub(last + 1, h[1])) end - spans[#spans + 1] = ui.Span(name:sub(h[1] + 1, h[2])):style(th.mgr.find_keyword) + spans[#spans + 1] = ui.Span(p(name:sub(h[1] + 1, h[2]))):style(th.mgr.find_keyword) last = h[2] end if last < #name then - spans[#spans + 1] = name:sub(last + 1) + spans[#spans + 1] = p(name:sub(last + 1)) end return ui.Line(spans) end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index 45249606..347495f7 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -64,7 +64,7 @@ function Status:name() return "" end - return " " .. h.name:gsub("\r", "?", 1) + return " " .. ui.printable(h.name) end function Status:perm() diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index 6196f4d0..f17b0876 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -1,19 +1,23 @@ +use std::borrow::Cow; + use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value}; use tracing::error; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span}}; use yazi_config::LAYOUT; use yazi_proxy::{AppProxy, HIDER}; +use yazi_shared::replace_to_printable; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { b"area" => area(lua)?, b"hide" => hide(lua)?, - b"width" => width(lua)?, + b"printable" => printable(lua)?, b"redraw" => redraw(lua)?, b"render" => render(lua)?, b"truncate" => truncate(lua)?, + b"width" => width(lua)?, _ => return Ok(Value::Nil), } .into_lua(lua) @@ -54,28 +58,12 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result { f.into_lua(lua) } -pub(super) fn width(lua: &Lua) -> mlua::Result { - 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::() { - Ok(line.width()) - } else if let Ok(span) = ud.borrow::() { - Ok(span.width()) - } else { - Err("expected a string, Line, or Span".into_lua_err())? - } - } - _ => Err("expected a string, Line, or Span".into_lua_err())?, +pub(super) fn printable(lua: &Lua) -> mlua::Result { + let f = lua.create_function(|lua, s: mlua::String| { + Ok(match replace_to_printable(&*s.as_bytes(), false, 1, true) { + Cow::Borrowed(_) => s, + Cow::Owned(new) => lua.create_string(&new)?, + }) })?; f.into_lua(lua) @@ -176,6 +164,33 @@ pub(super) fn truncate(lua: &Lua) -> mlua::Result { f.into_lua(lua) } +pub(super) fn width(lua: &Lua) -> mlua::Result { + 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::() { + Ok(line.width()) + } else if let Ok(span) = ud.borrow::() { + 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)] mod tests { use mlua::{Lua, chunk}; diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index b352a055..e79cbbec 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -6,7 +6,7 @@ use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Th use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use yazi_config::{THEME, YAZI, preview::PreviewWrap}; 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 SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new(); @@ -95,7 +95,7 @@ impl Highlighter { } 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 { Self::highlight_with(before, after, syntax.unwrap()).await? }) @@ -203,6 +203,16 @@ impl Highlighter { *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::() | 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 { diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index 590067b6..40bfe8f9 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -94,26 +94,48 @@ pub fn replace_vec_cow<'a>(v: &'a [u8], from: &[u8], to: &[u8]) -> Cow<'a, [u8]> Cow::Owned(out) } -pub fn replace_to_printable(s: &[String], tab_size: u8) -> String { - let mut buf = Vec::new(); - buf.try_reserve_exact(s.iter().map(|s| s.len()).sum::() | 15).unwrap_or_else(|_| panic!()); +pub fn replace_to_printable(b: &[u8], lf: bool, tab_size: u8, replacement: bool) -> Cow<'_, [u8]> { + // Fast path to skip over printable chars at the beginning of the string + 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()) { - match b { - b'\n' => buf.push(b'\n'), - b'\t' => { - buf.extend((0..tab_size).map(|_| b' ')); - } - b'\0'..=b'\x1F' => { + let (printable, rest) = b.split_at(printable_len); + + 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, c: u8, lf: bool, tab_size: u8, replacement: bool) { + match c { + b'\n' if lf => buf.push(b'\n'), + b'\t' => { + buf.extend((0..tab_size).map(|_| b' ')); + } + b'\0'..=b'\x1F' => { + if replacement { + buf.extend_from_slice(&[0xef, 0xbf, 0xbd]); + } else { 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(c), } - unsafe { String::from_utf8_unchecked(buf) } } diff --git a/yazi-shared/src/loc/able.rs b/yazi-shared/src/loc/able.rs index f25b9a3e..a87e17a1 100644 --- a/yazi-shared/src/loc/able.rs +++ b/yazi-shared/src/loc/able.rs @@ -107,7 +107,22 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path { where 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() } @@ -139,7 +154,18 @@ impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath { where 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() } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 59492801..d377b396 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -85,8 +85,8 @@ where { let path = path.as_path_view(); let Some(name) = path.file_name() else { - let uri = path.strip_prefix(P::empty()).unwrap().len(); - return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; + let p = path.strip_prefix(P::empty()).unwrap(); + return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData }; }; let name_len = name.len(); @@ -244,3 +244,38 @@ where 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(()) + } +} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index 49b7ef4f..fe3f1073 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -19,6 +19,10 @@ impl From for PathCow<'_> { 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> for PathBufDyn { fn from(value: PathCow<'_>) -> Self { value.into_owned() } } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index 846f139c..8e46e012 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -98,6 +98,10 @@ impl AsStrand for PathCow<'_> { } } +impl AsStrand for &PathCow<'_> { + fn as_strand(&self) -> Strand<'_> { (**self).as_strand() } +} + impl AsStrand for Strand<'_> { fn as_strand(&self) -> Strand<'_> { *self } } diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index a1cc5711..daddd691 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -109,7 +109,7 @@ impl<'a> Strand<'a> { let (skip, rest) = bytes.split_at(skip_len); let mut out = Vec::new(); - out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); + out.reserve_exact(bytes.len()); out.extend(skip); for &b in rest { diff --git a/yazi-shared/src/translit/traits.rs b/yazi-shared/src/translit/traits.rs index 590746f9..71d01824 100644 --- a/yazi-shared/src/translit/traits.rs +++ b/yazi-shared/src/translit/traits.rs @@ -19,7 +19,7 @@ impl Transliterator for &[u8] { // but instead of `+ 16` uses `| 15` to stay in the smallest allocation bucket // for short strings 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) }); for c in String::from_utf8_lossy(rest).chars() {