From d95375d49413e08943417c8b272387c1fee2dc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Sat, 28 Aug 2021 15:59:09 +0300 Subject: [PATCH] nu-path crate refactor (#3730) * Resolve rebase artifacts * Remove leftover dependencies on removed feature * Remove unnecessary 'pub' * Start taking notes and fooling around * Split canonicalize to two versions; Add TODOs One that takes `relative_to` and one that doesn't. More TODO notes. * Merge absolutize to and rename resolve_dots * Add custom absolutize fn and use it in path expand * Convert a couple of dunce::canonicalize to ours * Update nu-path description * Replace all canonicalize with nu-path version * Remove leftover dunce dependencies * Fix broken autocd with trailing slash Trailing slash is preserved *only* in paths that do not contain "." or "..". This should be fixed in the future to cover all paths but for now it at least covers basic cases. * Use dunce::canonicalize for canonicalizing * Alow cd recovery from non-existent cwd * Disable removed canonicalize functionality tests Remove unused import * Break down nu-path into separate modules * Remove unused public imports * Remove abundant cow mapping * Fix clippy warning * Reformulate old canonicalize tests to expand_path They wouldn't work with the new canonicalize. * Canonicalize also ~ and ndots; Unify path joining Also, add doc comments in nu_path::expansions. * Add comment * Avoid expanding ndots if path is not valid UTF-8 With this change, no lossy path->string conversion should happen in the nu-path crate. * Fmt * Slight expand_tilde refactor; Add doc comments * Start nu-path integration tests * Add tests TODO * Fix docstring typo * Fix some doc strings * Add README for nu-path crate * Add a couple of canonicalize tests * Add nu-path integration tests * Add trim trailing slashes tests * Update nu-path dependency * Remove unused import * Regenerate lockfile --- Cargo.lock | 125 ++-- Cargo.toml | 1 - crates/nu-command/Cargo.toml | 1 - crates/nu-command/src/classified/external.rs | 2 +- .../src/commands/core_commands/nu_plugin.rs | 4 +- .../src/commands/env/autoenv_trust.rs | 4 +- .../src/commands/env/autoenv_untrust.rs | 2 +- .../src/commands/filesystem/open.rs | 3 +- crates/nu-command/src/commands/path/expand.rs | 4 +- .../src/commands/platform/run_external.rs | 5 +- crates/nu-data/Cargo.toml | 1 + crates/nu-data/src/config/config_trust.rs | 2 +- crates/nu-engine/Cargo.toml | 1 - .../src/filesystem/filesystem_shell.rs | 29 +- crates/nu-engine/src/filesystem/utils.rs | 4 +- crates/nu-engine/src/plugin/build_plugin.rs | 5 +- crates/nu-engine/src/script.rs | 7 +- crates/nu-json/Cargo.toml | 2 +- crates/nu-json/tests/main.rs | 2 +- crates/nu-parser/Cargo.toml | 2 - crates/nu-parser/src/parse.rs | 13 +- crates/nu-path/Cargo.toml | 5 +- crates/nu-path/README.md | 3 + crates/nu-path/src/dots.rs | 259 +++++++++ crates/nu-path/src/expansions.rs | 75 +++ crates/nu-path/src/lib.rs | 538 +----------------- crates/nu-path/src/tilde.rs | 85 +++ crates/nu-path/src/util.rs | 4 + crates/nu-path/tests/canonicalize.rs | 412 ++++++++++++++ crates/nu-path/tests/expand_path.rs | 294 ++++++++++ crates/nu-path/tests/mod.rs | 3 + crates/nu-path/tests/util.rs | 45 ++ crates/nu-test-support/Cargo.toml | 1 + crates/nu-test-support/src/macros.rs | 4 +- crates/nu-test-support/src/playground/play.rs | 6 +- .../nu-test-support/src/playground/tests.rs | 2 +- tests/shell/environment/nu_env.rs | 17 +- tests/shell/mod.rs | 1 - 38 files changed, 1320 insertions(+), 653 deletions(-) create mode 100644 crates/nu-path/README.md create mode 100644 crates/nu-path/src/dots.rs create mode 100644 crates/nu-path/src/expansions.rs create mode 100644 crates/nu-path/src/tilde.rs create mode 100644 crates/nu-path/src/util.rs create mode 100644 crates/nu-path/tests/canonicalize.rs create mode 100644 crates/nu-path/tests/expand_path.rs create mode 100644 crates/nu-path/tests/mod.rs create mode 100644 crates/nu-path/tests/util.rs diff --git a/Cargo.lock b/Cargo.lock index 2787086c6..5a8aff800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "async-std" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" +checksum = "f8056f1455169ab86dd47b47391e4ab0cbd25410a70e9fe675544f49bafaf952" [[package]] name = "async-trait" @@ -451,9 +451,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "bzip2" @@ -688,9 +688,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -1614,7 +1614,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "fnv", "futures-core", "futures-sink", @@ -1743,7 +1743,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "fnv", "itoa", ] @@ -1754,7 +1754,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "http", "pin-project-lite", ] @@ -1782,11 +1782,11 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "futures-channel", "futures-core", "futures-util", @@ -1810,7 +1810,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "hyper", "native-tls", "tokio", @@ -2017,9 +2017,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "libgit2-sys" @@ -2083,9 +2083,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -2426,7 +2426,6 @@ name = "nu" version = "0.36.1" dependencies = [ "ctrlc", - "dunce", "futures 0.3.16", "hamcrest2", "itertools", @@ -2514,7 +2513,7 @@ dependencies = [ "base64", "bigdecimal-rs", "byte-unit", - "bytes 1.0.1", + "bytes 1.1.0", "calamine", "chrono", "chrono-tz", @@ -2527,7 +2526,6 @@ dependencies = [ "directories-next", "dirs-next", "dtparse", - "dunce", "eml-parser", "encoding_rs", "filesize", @@ -2640,6 +2638,7 @@ dependencies = [ "log", "nu-ansi-term", "nu-errors", + "nu-path", "nu-protocol", "nu-source", "nu-table", @@ -2669,7 +2668,6 @@ dependencies = [ "codespan-reporting", "derive-new", "dirs-next", - "dunce", "dyn-clone", "encoding_rs", "filesize", @@ -2734,9 +2732,9 @@ dependencies = [ name = "nu-json" version = "0.36.1" dependencies = [ - "dunce", "lazy_static", "linked-hash-map", + "nu-path", "nu-test-support", "num-traits", "regex", @@ -2751,7 +2749,6 @@ dependencies = [ "bigdecimal-rs", "codespan-reporting", "derive-new", - "dunce", "indexmap", "itertools", "log", @@ -2773,6 +2770,7 @@ version = "0.36.1" dependencies = [ "dirs-next", "dunce", + "nu-test-support", ] [[package]] @@ -2879,6 +2877,7 @@ dependencies = [ "hamcrest2", "indexmap", "nu-errors", + "nu-path", "nu-protocol", "nu-source", "nu-value-ext", @@ -3195,7 +3194,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" dependencies = [ - "num-bigint 0.4.0", + "num-bigint 0.4.1", "num-complex 0.4.0", "num-integer", "num-iter", @@ -3228,9 +3227,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +checksum = "76e97c412795abf6c24ba30055a8f20642ea57ca12875220b854cfa501bf1e48" dependencies = [ "autocfg", "num-integer", @@ -3320,7 +3319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", - "num-bigint 0.4.0", + "num-bigint 0.4.1", "num-integer", "num-traits", ] @@ -3376,9 +3375,9 @@ dependencies = [ [[package]] name = "object" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c" +checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" dependencies = [ "memchr", ] @@ -3477,9 +3476,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -3488,9 +3487,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "467fce6df07c66afb48b6284f13c724a1d5a13cbfcfdf5d5ce572c8313e0c722" dependencies = [ "cfg-if 1.0.0", "instant", @@ -3513,7 +3512,7 @@ dependencies = [ "chrono", "flate2", "lz4", - "num-bigint 0.4.0", + "num-bigint 0.4.1", "parquet-format", "rand 0.8.4", "snap", @@ -4231,7 +4230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" dependencies = [ "base64", - "bytes 1.0.1", + "bytes 1.1.0", "encoding_rs", "futures-core", "futures-util", @@ -4373,9 +4372,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustc-serialize" @@ -4418,7 +4417,7 @@ dependencies = [ "smallvec", "unicode-segmentation", "unicode-width", - "utf8parse 0.2.0", + "utf8parse", "winapi 0.3.9", ] @@ -4436,7 +4435,7 @@ checksum = "6b7327334dd66eab764647b4df9331a46487e933812351baf8fffdeb8a022711" dependencies = [ "async-trait", "base64", - "bytes 1.0.1", + "bytes 1.1.0", "chrono", "dyn-clone", "failure", @@ -4506,9 +4505,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +checksum = "19133a286e494cc3311c165c4676ccb1fd47bed45b55f9d71fbd784ad4cea6f8" dependencies = [ "core-foundation-sys", "libc", @@ -4627,9 +4626,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6375dbd828ed6964c3748e4ef6d18e7a175d408ffe184bca01698d0c73f915a9" +checksum = "ad104641f3c958dab30eb3010e834c2622d1f3f4c530fef1dee20ad9485f3c09" dependencies = [ "dtoa", "indexmap", @@ -4677,9 +4676,9 @@ checksum = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -4838,9 +4837,9 @@ dependencies = [ [[package]] name = "strip-ansi-escapes" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d63676e2abafa709460982ddc02a3bb586b6d15a49b75c212e06edd3933acee" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" dependencies = [ "vte", ] @@ -5162,12 +5161,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" dependencies = [ "autocfg", - "bytes 1.0.1", + "bytes 1.1.0", "libc", "memchr", "mio", @@ -5215,7 +5214,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "futures-core", "futures-sink", "log", @@ -5402,12 +5401,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" -[[package]] -name = "utf8parse" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" - [[package]] name = "utf8parse" version = "0.2.0" @@ -5449,11 +5442,23 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vte" -version = "0.3.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f42f536e22f7fcbb407639765c8fd78707a33109301f834a594758bedd6e8cf" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" dependencies = [ - "utf8parse 0.1.1", + "arrayvec 0.5.2", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8e995e1e0..b15f89726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,6 @@ itertools = "0.10.0" [dev-dependencies] nu-test-support = { version = "0.36.1", path="./crates/nu-test-support" } -dunce = "1.0.1" serial_test = "0.5.1" hamcrest2 = "0.3.0" rstest = "0.10.0" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 2eb19ab1e..3acb177d8 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -45,7 +45,6 @@ derive-new = "0.5.8" directories-next = "2.0.0" dirs-next = "2.0.0" dtparse = "1.2.0" -dunce = "1.0.1" eml-parser = "0.1.0" encoding_rs = "0.8.28" filesize = "0.2.0" diff --git a/crates/nu-command/src/classified/external.rs b/crates/nu-command/src/classified/external.rs index d8d816cf6..2725f97c7 100644 --- a/crates/nu-command/src/classified/external.rs +++ b/crates/nu-command/src/classified/external.rs @@ -104,7 +104,7 @@ fn run_with_stdin( let process_args = command_args .iter() .map(|(arg, _is_literal)| { - let arg = nu_path::expand_tilde_string(Cow::Borrowed(arg)); + let arg = nu_path::expand_tilde(arg).to_string_lossy().to_string(); #[cfg(not(windows))] { diff --git a/crates/nu-command/src/commands/core_commands/nu_plugin.rs b/crates/nu-command/src/commands/core_commands/nu_plugin.rs index 1dff1d088..0f91a6c14 100644 --- a/crates/nu-command/src/commands/core_commands/nu_plugin.rs +++ b/crates/nu-command/src/commands/core_commands/nu_plugin.rs @@ -4,7 +4,7 @@ use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_path::canonicalize; +use nu_path::canonicalize_with; use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; use nu_source::Tagged; @@ -56,7 +56,7 @@ impl WholeStreamCommand for SubCommand { tag, }) = load_path { - let path = canonicalize(shell_manager.path(), load_path).map_err(|_| { + let path = canonicalize_with(load_path, shell_manager.path()).map_err(|_| { ShellError::labeled_error( "Cannot load plugins from directory", "directory not found", diff --git a/crates/nu-command/src/commands/env/autoenv_trust.rs b/crates/nu-command/src/commands/env/autoenv_trust.rs index a1f9a50c7..d3f62ca05 100644 --- a/crates/nu-command/src/commands/env/autoenv_trust.rs +++ b/crates/nu-command/src/commands/env/autoenv_trust.rs @@ -31,12 +31,12 @@ impl WholeStreamCommand for AutoenvTrust { value: UntaggedValue::Primitive(Primitive::String(ref path)), tag: _, }) => { - let mut dir = fs::canonicalize(path)?; + let mut dir = nu_path::canonicalize(path)?; dir.push(".nu-env"); dir } _ => { - let mut dir = fs::canonicalize(std::env::current_dir()?)?; + let mut dir = nu_path::canonicalize(std::env::current_dir()?)?; dir.push(".nu-env"); dir } diff --git a/crates/nu-command/src/commands/env/autoenv_untrust.rs b/crates/nu-command/src/commands/env/autoenv_untrust.rs index 385ea85ca..a4e2098d6 100644 --- a/crates/nu-command/src/commands/env/autoenv_untrust.rs +++ b/crates/nu-command/src/commands/env/autoenv_untrust.rs @@ -30,7 +30,7 @@ impl WholeStreamCommand for AutoenvUntrust { value: UntaggedValue::Primitive(Primitive::String(ref path)), tag: _, }) => { - let mut dir = fs::canonicalize(path)?; + let mut dir = nu_path::canonicalize(path)?; dir.push(".nu-env"); dir } diff --git a/crates/nu-command/src/commands/filesystem/open.rs b/crates/nu-command/src/commands/filesystem/open.rs index 797c670c5..a427061dc 100644 --- a/crates/nu-command/src/commands/filesystem/open.rs +++ b/crates/nu-command/src/commands/filesystem/open.rs @@ -6,6 +6,7 @@ use log::debug; use nu_engine::StringOrBinary; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; +use nu_path::canonicalize; use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::{AnchorLocation, Span, Tagged}; use std::path::{Path, PathBuf}; @@ -193,7 +194,7 @@ pub fn fetch( // TODO: I don't understand the point of this? Maybe for better error reporting let mut cwd = PathBuf::from(cwd); cwd.push(location); - let nice_location = dunce::canonicalize(&cwd).map_err(|e| match e.kind() { + let nice_location = canonicalize(&cwd).map_err(|e| match e.kind() { std::io::ErrorKind::NotFound => ShellError::labeled_error( format!("Cannot find file {:?}", cwd), "cannot find file", diff --git a/crates/nu-command/src/commands/path/expand.rs b/crates/nu-command/src/commands/path/expand.rs index 235af8176..9b086dfb4 100644 --- a/crates/nu-command/src/commands/path/expand.rs +++ b/crates/nu-command/src/commands/path/expand.rs @@ -2,7 +2,7 @@ use super::{operate, PathSubcommandArguments}; use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_path::expand_path; +use nu_path::{canonicalize, expand_path}; use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::Span; use std::{borrow::Cow, path::Path}; @@ -95,7 +95,7 @@ impl WholeStreamCommand for PathExpand { } fn action(path: &Path, tag: Tag, args: &PathExpandArguments) -> Value { - if let Ok(p) = dunce::canonicalize(path) { + if let Ok(p) = canonicalize(path) { UntaggedValue::filepath(p).into_value(tag) } else if args.strict { Value::error(ShellError::labeled_error( diff --git a/crates/nu-command/src/commands/platform/run_external.rs b/crates/nu-command/src/commands/platform/run_external.rs index 400d76b45..ce61f999b 100644 --- a/crates/nu-command/src/commands/platform/run_external.rs +++ b/crates/nu-command/src/commands/platform/run_external.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use nu_engine::WholeStreamCommand; use nu_engine::{evaluate_baseline_expr, shell::CdArgs}; use nu_errors::ShellError; +use nu_path::{canonicalize, trim_trailing_slash}; use nu_protocol::{ hir::{ExternalArgs, ExternalCommand, SpannedExpression}, Primitive, UntaggedValue, @@ -137,10 +138,10 @@ fn maybe_autocd_dir(cmd: &ExternalCommand, ctx: &mut EvaluationContext) -> Optio let path_name = if name.ends_with(std::path::is_separator) || (cmd.args.is_empty() && PathBuf::from(name).is_dir() - && dunce::canonicalize(name).is_ok() + && canonicalize(name).is_ok() && !ctx.host().lock().is_external_cmd(name)) { - Some(name) + Some(trim_trailing_slash(name)) } else { None }; diff --git a/crates/nu-data/Cargo.toml b/crates/nu-data/Cargo.toml index 26a12e296..f913f4dae 100644 --- a/crates/nu-data/Cargo.toml +++ b/crates/nu-data/Cargo.toml @@ -30,6 +30,7 @@ sys-locale = "0.1.0" toml = "0.5.8" nu-errors = { version = "0.36.1", path="../nu-errors" } +nu-path = { version = "0.36.1", path="../nu-path" } nu-protocol = { version = "0.36.1", path="../nu-protocol" } nu-source = { version = "0.36.1", path="../nu-source" } nu-table = { version = "0.36.1", path="../nu-table" } diff --git a/crates/nu-data/src/config/config_trust.rs b/crates/nu-data/src/config/config_trust.rs index 5279ae6fc..01b516ed6 100644 --- a/crates/nu-data/src/config/config_trust.rs +++ b/crates/nu-data/src/config/config_trust.rs @@ -21,7 +21,7 @@ impl Trusted { pub fn is_file_trusted(nu_env_file: &Path, content: &[u8]) -> Result { let contentdigest = Sha256::digest(content).as_slice().to_vec(); - let nufile = std::fs::canonicalize(nu_env_file)?; + let nufile = nu_path::canonicalize(nu_env_file)?; let trusted = read_trusted()?; Ok(trusted.files.get(&nufile.to_string_lossy().to_string()) == Some(&contentdigest)) diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index 9b6bf18f6..00843006c 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -31,7 +31,6 @@ bytes = "0.5.6" chrono = { version="0.4.19", features=["serde"] } derive-new = "0.5.8" dirs-next = "2.0.0" -dunce = "1.0.1" encoding_rs = "0.8.28" filesize = "0.2.0" fs_extra = "1.2.0" diff --git a/crates/nu-engine/src/filesystem/filesystem_shell.rs b/crates/nu-engine/src/filesystem/filesystem_shell.rs index d4c49dd10..6b736de73 100644 --- a/crates/nu-engine/src/filesystem/filesystem_shell.rs +++ b/crates/nu-engine/src/filesystem/filesystem_shell.rs @@ -9,7 +9,7 @@ use crate::{ }; use encoding_rs::Encoding; use nu_data::config::LocalConfigDiff; -use nu_path::canonicalize; +use nu_path::{canonicalize, canonicalize_with, expand_path_with}; use nu_protocol::{CommandAction, ConfigPath, TaggedDictBuilder, Value}; use nu_source::{Span, Tag}; use nu_stream::{ActionStream, Interruptible, IntoActionStream, OutputStream}; @@ -77,7 +77,7 @@ impl FilesystemShell { path: String, mode: FilesystemShellMode, ) -> Result { - let path = canonicalize(std::env::current_dir()?, &path)?; + let path = canonicalize_with(&path, std::env::current_dir()?)?; let path = path.display().to_string(); let last_path = path.clone(); @@ -237,13 +237,20 @@ impl Shell for FilesystemShell { if target == Path::new("-") { PathBuf::from(&self.last_path) } else { - let path = canonicalize(self.path(), target).map_err(|_| { - ShellError::labeled_error( + // Extra expand attempt allows cd from /home/user/non-existent-dir/.. + // to /home/user + let path = match canonicalize_with(&target, self.path()) { + Ok(p) => p, + _ => expand_path_with(&target, self.path()), + }; + + if !path.exists() { + return Err(ShellError::labeled_error( "Cannot change to directory", "directory not found", &tag, - ) - })?; + )); + } if !path.is_dir() { return Err(ShellError::labeled_error( @@ -291,7 +298,7 @@ impl Shell for FilesystemShell { //Loading local configs in script mode, makes scripts behave different on different //filesystems and might therefore surprise users. That's why we only load them in cli mode. if self.is_cli() { - match dunce::canonicalize(self.path()) { + match canonicalize(self.path()) { Err(e) => { let err = ShellError::untagged_runtime_error(format!( "Could not get absolute path from current fs shell. The error was: {:?}", @@ -388,7 +395,7 @@ impl Shell for FilesystemShell { if entry.is_file() { let sources = sources.paths_applying_with(|(source_file, _depth_level)| { if destination.is_dir() { - let mut dest = canonicalize(&path, &dst.item)?; + let mut dest = canonicalize_with(&dst.item, &path)?; if let Some(name) = entry.file_name() { dest.push(name); } @@ -427,7 +434,7 @@ impl Shell for FilesystemShell { let sources = sources.paths_applying_with(|(source_file, depth_level)| { let mut dest = destination.clone(); - let path = canonicalize(&path, &source_file)?; + let path = canonicalize_with(&source_file, &path)?; let comps: Vec<_> = path .components() @@ -773,7 +780,7 @@ impl Shell for FilesystemShell { fn pwd(&self, args: CommandArgs) -> Result { let path = PathBuf::from(self.path()); - let p = match dunce::canonicalize(path.as_path()) { + let p = match canonicalize(path.as_path()) { Ok(p) => p, Err(_) => { return Err(ShellError::labeled_error( @@ -792,7 +799,7 @@ impl Shell for FilesystemShell { fn set_path(&mut self, path: String) { let pathbuf = PathBuf::from(&path); - let path = match canonicalize(self.path(), pathbuf.as_path()) { + let path = match canonicalize_with(pathbuf.as_path(), self.path()) { Ok(path) => { let _ = std::env::set_current_dir(&path); std::env::set_var("PWD", &path); diff --git a/crates/nu-engine/src/filesystem/utils.rs b/crates/nu-engine/src/filesystem/utils.rs index 24bde5342..27a58647e 100644 --- a/crates/nu-engine/src/filesystem/utils.rs +++ b/crates/nu-engine/src/filesystem/utils.rs @@ -1,5 +1,5 @@ use nu_errors::ShellError; -use nu_path::canonicalize; +use nu_path::canonicalize_with; use std::path::{Path, PathBuf}; #[derive(Default)] @@ -47,7 +47,7 @@ impl FileStructure { } fn build(&mut self, src: &Path, lvl: usize) -> Result<(), ShellError> { - let source = canonicalize(std::env::current_dir()?, src)?; + let source = canonicalize_with(src, std::env::current_dir()?)?; if source.is_dir() { for entry in std::fs::read_dir(src)? { diff --git a/crates/nu-engine/src/plugin/build_plugin.rs b/crates/nu-engine/src/plugin/build_plugin.rs index 37f1c8932..0c790dfdb 100644 --- a/crates/nu-engine/src/plugin/build_plugin.rs +++ b/crates/nu-engine/src/plugin/build_plugin.rs @@ -1,6 +1,7 @@ use crate::plugin::run_plugin::PluginCommandBuilder; use log::trace; use nu_errors::ShellError; +use nu_path::canonicalize; use nu_plugin::jsonrpc::JsonRpc; use nu_protocol::{Signature, Value}; use std::io::{BufRead, BufReader, Write}; @@ -48,7 +49,7 @@ pub fn build_plugin_command( let request_raw = serde_json::to_string(&request)?; trace!(target: "nu::load", "plugin infrastructure config -> path {:#?}, request {:?}", &path, &request_raw); stdin.write_all(format!("{}\n", request_raw).as_bytes())?; - let path = dunce::canonicalize(path)?; + let path = canonicalize(path)?; let mut input = String::new(); let result = match reader.read_line(&mut input) { @@ -134,7 +135,7 @@ pub fn scan( let is_executable = { #[cfg(windows)] { - bin_name.ends_with(".exe") + bin_name.ends_with(".exe") || bin_name.ends_with(".bat") || bin_name.ends_with(".cmd") || bin_name.ends_with(".py") diff --git a/crates/nu-engine/src/script.rs b/crates/nu-engine/src/script.rs index b0840bd39..796b4e491 100644 --- a/crates/nu-engine/src/script.rs +++ b/crates/nu-engine/src/script.rs @@ -1,7 +1,7 @@ use crate::{evaluate::internal::InternalIterator, maybe_print_errors, run_block, shell::CdArgs}; use crate::{BufCodecReader, MaybeTextCodec, StringOrBinary}; use nu_errors::ShellError; -use nu_path::canonicalize; +use nu_path::{canonicalize_with, trim_trailing_slash}; use nu_protocol::hir::{ Call, ClassifiedCommand, Expression, ExternalRedirection, InternalCommand, Literal, NamedArguments, SpannedExpression, @@ -66,7 +66,6 @@ pub fn process_script( let (block, err) = nu_parser::parse(line, span_offset, &ctx.scope); debug!("{:#?}", block); - //println!("{:#?}", pipeline); if let Some(failure) = err { return LineResult::Error(line.to_string(), failure.into()); @@ -116,7 +115,7 @@ pub fn process_script( .as_ref() .map(NamedArguments::is_empty) .unwrap_or(true) - && canonicalize(ctx.shell_manager().path(), name).is_ok() + && canonicalize_with(name, ctx.shell_manager().path()).is_ok() && Path::new(&name).is_dir() && !ctx.host().lock().is_external_cmd(name) { @@ -160,6 +159,8 @@ pub fn process_script( } }; + let path = trim_trailing_slash(&path); + let cd_args = CdArgs { path: Some(Tagged { item: PathBuf::from(path), diff --git a/crates/nu-json/Cargo.toml b/crates/nu-json/Cargo.toml index 7b838880f..1c4bd6ddf 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -20,6 +20,6 @@ lazy_static = "1" linked-hash-map = { version="0.5", optional=true } [dev-dependencies] +nu-path = { version = "0.36.1", path="../nu-path" } nu-test-support = { version = "0.36.1", path="../nu-test-support" } serde_json = "1.0.39" -dunce = "1.0.1" diff --git a/crates/nu-json/tests/main.rs b/crates/nu-json/tests/main.rs index b7961bc39..9b20a6b49 100644 --- a/crates/nu-json/tests/main.rs +++ b/crates/nu-json/tests/main.rs @@ -26,7 +26,7 @@ fn txt(text: &str) -> String { fn hjson_expectations() -> PathBuf { let assets = nu_test_support::fs::assets().join("nu_json"); - dunce::canonicalize(assets.clone()).unwrap_or_else(|e| { + nu_path::canonicalize(assets.clone()).unwrap_or_else(|e| { panic!( "Couldn't canonicalize hjson assets path {}: {:?}", assets.display(), diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index 950dc2d0e..1fdea29a5 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -18,8 +18,6 @@ serde = "1.0" itertools = "0.10.0" smart-default = "0.6.0" -dunce = "1.0.1" - nu-errors = { version = "0.36.1", path="../nu-errors" } nu-data = { version = "0.36.1", path="../nu-data" } nu-path = { version = "0.36.1", path="../nu-path" } diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index 2d619e1ab..79af9e02b 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -1,11 +1,10 @@ -use std::borrow::Cow; use std::{path::PathBuf, sync::Arc}; use bigdecimal::BigDecimal; use indexmap::IndexMap; use log::trace; use nu_errors::{ArgumentError, ParseError}; -use nu_path::{expand_path, expand_path_string}; +use nu_path::expand_path; use nu_protocol::hir::{ self, Binary, Block, Call, ClassifiedCommand, Expression, ExternalRedirection, Flag, FlagKind, Group, InternalCommand, Member, NamedArguments, Operator, Pipeline, RangeOperator, @@ -954,8 +953,8 @@ fn parse_arg( ) } SyntaxShape::GlobPattern => { - let trimmed = Cow::Owned(trim_quotes(&lite_arg.item)); - let expanded = expand_path_string(trimmed).to_string(); + let trimmed = trim_quotes(&lite_arg.item); + let expanded = expand_path(trimmed).to_string_lossy().to_string(); ( SpannedExpression::new(Expression::glob_pattern(expanded), lite_arg.span), None, @@ -972,7 +971,7 @@ fn parse_arg( SyntaxShape::FilePath => { let trimmed = trim_quotes(&lite_arg.item); let path = PathBuf::from(trimmed); - let expanded = expand_path(Cow::Owned(path)).to_path_buf(); + let expanded = expand_path(path); ( SpannedExpression::new(Expression::FilePath(expanded), lite_arg.span), None, @@ -1659,8 +1658,8 @@ fn parse_external_call( ) -> (Option, Option) { let mut error = None; let name = lite_cmd.parts[0].clone().map(|v| { - let trimmed = Cow::Owned(trim_quotes(&v)); - expand_path_string(trimmed).to_string() + let trimmed = trim_quotes(&v); + expand_path(trimmed).to_string_lossy().to_string() }); let mut args = vec![]; diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml index 08ce9f140..d55373274 100644 --- a/crates/nu-path/Cargo.toml +++ b/crates/nu-path/Cargo.toml @@ -1,6 +1,6 @@ [package] authors = ["The Nu Project Contributors"] -description = "Nushell parser" +description = "Path handling library for Nushell" edition = "2018" license = "MIT" name = "nu-path" @@ -9,3 +9,6 @@ version = "0.36.1" [dependencies] dirs-next = "2.0.0" dunce = "1.0.1" + +[dev-dependencies] +nu-test-support = { version = "0.36.1", path="../nu-test-support" } diff --git a/crates/nu-path/README.md b/crates/nu-path/README.md new file mode 100644 index 000000000..382fd687b --- /dev/null +++ b/crates/nu-path/README.md @@ -0,0 +1,3 @@ +# nu-path + +This crate takes care of path handling in Nushell, such as canonicalization and component expansion, as well as other path-related utilities. diff --git a/crates/nu-path/src/dots.rs b/crates/nu-path/src/dots.rs new file mode 100644 index 000000000..b6025c479 --- /dev/null +++ b/crates/nu-path/src/dots.rs @@ -0,0 +1,259 @@ +use std::path::{is_separator, Component, Path, PathBuf}; + +const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; + +fn handle_dots_push(string: &mut String, count: u8) { + if count < 1 { + return; + } + + if count == 1 { + string.push('.'); + return; + } + + for _ in 0..(count - 1) { + string.push_str(EXPAND_STR); + } + + string.pop(); // remove last '/' +} + +/// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g., +/// "..." into "../..", "...." into "../../../", etc. +pub fn expand_ndots(path: impl AsRef) -> PathBuf { + // Check if path is valid UTF-8 and if not, return it as it is to avoid breaking it via string + // conversion. + let path_str = match path.as_ref().to_str() { + Some(s) => s, + None => return path.as_ref().into(), + }; + + // find if we need to expand any >2 dot paths and early exit if not + let mut dots_count = 0u8; + let ndots_present = { + for chr in path_str.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) && (dots_count > 2) { + // this path component had >2 dots + break; + } + + dots_count = 0; + } + } + + dots_count > 2 + }; + + if !ndots_present { + return path.as_ref().into(); + } + + let mut dots_count = 0u8; + let mut expanded = String::new(); + for chr in path_str.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) { + // check for dots expansion only at path component boundaries + handle_dots_push(&mut expanded, dots_count); + dots_count = 0; + } else { + // got non-dot within path component => do not expand any dots + while dots_count > 0 { + expanded.push('.'); + dots_count -= 1; + } + } + expanded.push(chr); + } + } + + handle_dots_push(&mut expanded, dots_count); + + expanded.into() +} + +/// Expand "." and ".." into nothing and parent directory, respectively. +pub fn expand_dots(path: impl AsRef) -> PathBuf { + let path = path.as_ref(); + + // Early-exit if path does not contain '.' or '..' + if !path + .components() + .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) + { + return path.into(); + } + + let mut result = PathBuf::with_capacity(path.as_os_str().len()); + + // Only pop/skip path elements if the previous one was an actual path element + let prev_is_normal = |p: &Path| -> bool { + p.components() + .next_back() + .map(|c| std::matches!(c, Component::Normal(_))) + .unwrap_or(false) + }; + + path.components().for_each(|component| match component { + Component::ParentDir if prev_is_normal(&result) => { + result.pop(); + } + Component::CurDir if prev_is_normal(&result) => {} + _ => result.push(component), + }); + + dunce::simplified(&result).to_path_buf() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expand_two_dots() { + let path = Path::new("/foo/bar/.."); + + assert_eq!( + PathBuf::from("/foo"), // missing path + expand_dots(path) + ); + } + + #[test] + fn expand_dots_with_curdir() { + let path = Path::new("/foo/./bar/./baz"); + + assert_eq!(PathBuf::from("/foo/bar/baz"), expand_dots(path)); + } + + fn check_ndots_expansion(expected: &str, s: &str) { + let expanded = expand_ndots(Path::new(s)); + assert_eq!(Path::new(expected), &expanded); + } + + // common tests + #[test] + fn string_without_ndots() { + check_ndots_expansion("../hola", "../hola"); + } + + #[test] + fn string_with_three_ndots_and_chars() { + check_ndots_expansion("a...b", "a...b"); + } + + #[test] + fn string_with_two_ndots_and_chars() { + check_ndots_expansion("a..b", "a..b"); + } + + #[test] + fn string_with_one_dot_and_chars() { + check_ndots_expansion("a.b", "a.b"); + } + + #[test] + fn expand_dots_double_dots_no_change() { + // Can't resolve this as we don't know our parent dir + assert_eq!(Path::new(".."), expand_dots(Path::new(".."))); + } + + #[test] + fn expand_dots_single_dot_no_change() { + // Can't resolve this as we don't know our current dir + assert_eq!(Path::new("."), expand_dots(Path::new("."))); + } + + #[test] + fn expand_dots_multi_single_dots_no_change() { + assert_eq!(Path::new("././."), expand_dots(Path::new("././."))); + } + + #[test] + fn expand_multi_double_dots_no_change() { + assert_eq!(Path::new("../../../"), expand_dots(Path::new("../../../"))); + } + + #[test] + fn expand_dots_no_change_with_dirs() { + // Can't resolve this as we don't know our parent dir + assert_eq!( + Path::new("../../../dir1/dir2/"), + expand_dots(Path::new("../../../dir1/dir2")) + ); + } + + #[test] + fn expand_dots_simple() { + assert_eq!(Path::new("/foo"), expand_dots(Path::new("/foo/bar/.."))); + } + + #[test] + fn expand_dots_complex() { + assert_eq!( + Path::new("/test"), + expand_dots(Path::new("/foo/./bar/../../test/././test2/../")) + ); + } + + #[cfg(windows)] + mod windows { + use super::*; + + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"..\..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + r"a...b/./c..d/../e.f/..\..\..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion(r"..\../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*["); + } + } + + #[cfg(not(windows))] + mod non_windows { + use super::*; + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"../..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + "a...b/./c..d/../e.f/../../..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion("../../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*["); + } + } +} diff --git a/crates/nu-path/src/expansions.rs b/crates/nu-path/src/expansions.rs new file mode 100644 index 000000000..3393a5793 --- /dev/null +++ b/crates/nu-path/src/expansions.rs @@ -0,0 +1,75 @@ +use std::io; +use std::path::{Path, PathBuf}; + +use super::dots::{expand_dots, expand_ndots}; +use super::tilde::expand_tilde; + +// Join a path relative to another path. Paths starting with tilde are considered as absolute. +fn join_path_relative(path: P, relative_to: Q) -> PathBuf +where + P: AsRef, + Q: AsRef, +{ + let path = path.as_ref(); + let relative_to = relative_to.as_ref(); + + if path == Path::new(".") { + // Joining a Path with '.' appends a '.' at the end, making the prompt + // more ugly - so we don't do anything, which should result in an equal + // path on all supported systems. + relative_to.into() + } else if path.starts_with("~") { + // do not end up with "/some/path/~" + path.into() + } else { + relative_to.join(path) + } +} + +/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its +/// absolute form. +/// +/// Fails under the same conditions as +/// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html). +pub fn canonicalize(path: impl AsRef) -> io::Result { + let path = expand_tilde(path); + let path = expand_ndots(path); + + dunce::canonicalize(path) +} + +/// Same as canonicalize() but the input path is specified relative to another path +pub fn canonicalize_with(path: P, relative_to: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + let path = join_path_relative(path, relative_to); + + canonicalize(path) +} + +/// Resolve only path components (tilde, ., .., ...+), if possible. +/// +/// The function works in a "best effort" mode: It does not fail but rather returns the unexpanded +/// version if the expansion is not possible. +/// +/// Furthermore, unlike canonicalize(), it does not use sys calls (such as readlink). +/// +/// Does not convert to absolute form nor does it resolve symlinks. +pub fn expand_path(path: impl AsRef) -> PathBuf { + let path = expand_tilde(path); + let path = expand_ndots(path); + expand_dots(path) +} + +/// Same as expand_path() but the input path is specified relative to another path +pub fn expand_path_with(path: P, relative_to: Q) -> PathBuf +where + P: AsRef, + Q: AsRef, +{ + let path = join_path_relative(path, relative_to); + + expand_path(path) +} diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index cd1e565e2..9606bc15c 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -1,530 +1,8 @@ -use std::borrow::Cow; -use std::io; -use std::path::{Component, Path, PathBuf}; - -// Utility for applying a function that can only be called on the borrowed type of the Cow -// and also returns a ref. If the Cow is a borrow, we can return the same borrow but an -// owned value needs extra handling because the returned valued has to be owned as well -pub fn cow_map_by_ref(c: Cow<'_, B>, f: F) -> Cow<'_, B> -where - B: ToOwned + ?Sized, - O: AsRef, - F: FnOnce(&B) -> &B, -{ - match c { - Cow::Borrowed(b) => Cow::Borrowed(f(b)), - Cow::Owned(o) => Cow::Owned(f(o.as_ref()).to_owned()), - } -} - -// Utility for applying a function over Cow<'a, Path> over a Cow<'a, str> while avoiding unnecessary conversions -fn cow_map_str_path<'a, F>(c: Cow<'a, str>, f: F) -> Cow<'a, str> -where - F: FnOnce(Cow<'a, Path>) -> Cow<'a, Path>, -{ - let ret = match c { - Cow::Borrowed(b) => f(Cow::Borrowed(Path::new(b))), - Cow::Owned(o) => f(Cow::Owned(PathBuf::from(o))), - }; - - match ret { - Cow::Borrowed(expanded) => expanded.to_string_lossy(), - Cow::Owned(expanded) => Cow::Owned(expanded.to_string_lossy().to_string()), - } -} - -// Utility for applying a function over Cow<'a, str> over a Cow<'a, Path> while avoiding unnecessary conversions -fn cow_map_path_str<'a, F>(c: Cow<'a, Path>, f: F) -> Cow<'a, Path> -where - F: FnOnce(Cow<'a, str>) -> Cow<'a, str>, -{ - let ret = match c { - Cow::Borrowed(path) => f(path.to_string_lossy()), - Cow::Owned(buf) => f(Cow::Owned(buf.to_string_lossy().to_string())), - }; - - match ret { - Cow::Borrowed(expanded) => Cow::Borrowed(Path::new(expanded)), - Cow::Owned(expanded) => Cow::Owned(PathBuf::from(expanded)), - } -} - -const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; -fn handle_dots_push(string: &mut String, count: u8) { - if count < 1 { - return; - } - - if count == 1 { - string.push('.'); - return; - } - - for _ in 0..(count - 1) { - string.push_str(EXPAND_STR); - } - - string.pop(); // remove last '/' -} - -// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. -// ... into ../.. -// .... into ../../../ -fn expand_ndots_string(path: Cow<'_, str>) -> Cow<'_, str> { - use std::path::is_separator; - // find if we need to expand any >2 dot paths and early exit if not - let mut dots_count = 0u8; - let ndots_present = { - for chr in path.chars() { - if chr == '.' { - dots_count += 1; - } else { - if is_separator(chr) && (dots_count > 2) { - // this path component had >2 dots - break; - } - - dots_count = 0; - } - } - - dots_count > 2 - }; - - if !ndots_present { - return path; - } - - let mut dots_count = 0u8; - let mut expanded = String::new(); - for chr in path.chars() { - if chr == '.' { - dots_count += 1; - } else { - if is_separator(chr) { - // check for dots expansion only at path component boundaries - handle_dots_push(&mut expanded, dots_count); - dots_count = 0; - } else { - // got non-dot within path component => do not expand any dots - while dots_count > 0 { - expanded.push('.'); - dots_count -= 1; - } - } - expanded.push(chr); - } - } - - handle_dots_push(&mut expanded, dots_count); - - expanded.into() -} - -// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. -// ... into ../.. -// .... into ../../../ -fn expand_ndots(path: Cow<'_, Path>) -> Cow<'_, Path> { - cow_map_path_str(path, expand_ndots_string) -} - -pub fn absolutize(relative_to: P, path: Q) -> PathBuf -where - P: AsRef, - Q: AsRef, -{ - let path = if path.as_ref() == Path::new(".") { - // Joining a Path with '.' appends a '.' at the end, making the prompt - // more ugly - so we don't do anything, which should result in an equal - // path on all supported systems. - relative_to.as_ref().to_owned() - } else if path.as_ref().starts_with("~") { - expand_tilde(Cow::Borrowed(path.as_ref())).to_path_buf() - } else { - relative_to.as_ref().join(path) - }; - - let (relative_to, path) = { - let components: Vec<_> = path.components().collect(); - let separator = components - .iter() - .enumerate() - .find(|(_, c)| c == &&Component::CurDir || c == &&Component::ParentDir); - - if let Some((index, _)) = separator { - let (absolute, relative) = components.split_at(index); - let absolute: PathBuf = absolute.iter().collect(); - let relative: PathBuf = relative.iter().collect(); - - (absolute, relative) - } else { - ( - relative_to.as_ref().to_path_buf(), - components.iter().collect::(), - ) - } - }; - - let path = if path.is_relative() { - let mut result = relative_to; - path.components().for_each(|component| match component { - Component::ParentDir => { - result.pop(); - } - Component::Normal(normal) => result.push(normal), - _ => {} - }); - - result - } else { - path - }; - - dunce::simplified(&path).to_path_buf() -} - -pub fn canonicalize(relative_to: P, path: Q) -> io::Result -where - P: AsRef, - Q: AsRef, -{ - let absolutized = absolutize(&relative_to, path); - let path = match std::fs::read_link(&absolutized) { - Ok(resolved) => { - let parent = absolutized.parent().unwrap_or(&absolutized); - absolutize(parent, resolved) - } - - Err(e) => { - if absolutized.exists() { - absolutized - } else { - return Err(e); - } - } - }; - - Ok(dunce::simplified(&path).to_path_buf()) -} - -// Expansion logic lives here to enable testing without depending on dirs-next -fn expand_tilde_with(path: Cow<'_, Path>, home: Option) -> Cow<'_, Path> { - if !path.starts_with("~") { - return path; - } - - match home { - None => path, - Some(mut h) => { - if h == Path::new("/") { - // Corner case: `h` root directory; - // don't prepend extra `/`, just drop the tilde. - cow_map_by_ref(path, |p: &Path| { - p.strip_prefix("~").expect("cannot strip ~ prefix") - }) - } else { - h.push(path.strip_prefix("~/").expect("cannot strip ~/ prefix")); - Cow::Owned(h) - } - } - } -} - -pub fn expand_tilde(path: Cow<'_, Path>) -> Cow<'_, Path> { - expand_tilde_with(path, dirs_next::home_dir()) -} - -pub fn expand_tilde_string(path: Cow<'_, str>) -> Cow<'_, str> { - cow_map_str_path(path, expand_tilde) -} - -// Remove "." and ".." in a path. Prefix ".." are not removed as we don't have access to the -// current dir. This is merely 'string manipulation'. Does not handle "...+", see expand_ndots for that -pub fn resolve_dots(path: Cow<'_, Path>) -> Cow<'_, Path> { - debug_assert!(!path.components().any(|c| std::matches!(c, Component::Normal(os_str) if os_str.to_string_lossy().starts_with("..."))), "Unexpected ndots!"); - if !path - .components() - .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) - { - return path; - } - - let mut result = PathBuf::with_capacity(path.as_os_str().len()); - - // Only pop/skip path elements if the previous one was an actual path element - let prev_is_normal = |p: &Path| -> bool { - p.components() - .next_back() - .map(|c| std::matches!(c, Component::Normal(_))) - .unwrap_or(false) - }; - path.as_ref() - .components() - .for_each(|component| match component { - Component::ParentDir if prev_is_normal(&result) => { - result.pop(); - } - Component::CurDir if prev_is_normal(&result) => {} - _ => result.push(component), - }); - - Cow::Owned(dunce::simplified(&result).to_path_buf()) -} - -// Expands ~ to home and shortens paths by removing unecessary ".." and "." -// where possible. Also expands "...+" appropriately. -pub fn expand_path(path: Cow<'_, Path>) -> Cow<'_, Path> { - let path = expand_tilde(path); - let path = expand_ndots(path); - resolve_dots(path) -} - -pub fn expand_path_string(path: Cow<'_, str>) -> Cow<'_, str> { - cow_map_str_path(path, expand_path) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io; - - #[test] - fn absolutize_two_dots() { - let relative_to = Path::new("/foo/bar"); - let path = Path::new(".."); - - assert_eq!( - PathBuf::from("/foo"), // missing path - absolutize(relative_to, path) - ); - } - - #[test] - fn absolutize_with_curdir() { - let relative_to = Path::new("/foo"); - let path = Path::new("./bar/./baz"); - - assert!(!absolutize(relative_to, path) - .to_str() - .unwrap() - .contains('.')); - } - - #[test] - fn canonicalize_should_succeed() -> io::Result<()> { - let relative_to = Path::new("/foo/bar"); - let path = Path::new("../.."); - - assert_eq!( - PathBuf::from("/"), // existing path - canonicalize(relative_to, path)?, - ); - - Ok(()) - } - - #[test] - fn canonicalize_should_fail() { - let relative_to = Path::new("/foo/bar/baz"); // '/foo' is missing - let path = Path::new("../.."); - - assert!(canonicalize(relative_to, path).is_err()); - } - - fn check_ndots_expansion(expected: &str, s: &str) { - let expanded = expand_ndots(Cow::Borrowed(Path::new(s))); - // If we don't expect expansion, verify that we get a borrow back and no PathBuf creation has been made - if expected == s { - assert!( - std::matches!(expanded, Cow::Borrowed(_)), - "No PathBuf should be needed here (unnecessary allocation)" - ); - } - assert_eq!(Path::new(expected), &expanded); - } - - // common tests - #[test] - fn string_without_ndots() { - check_ndots_expansion("../hola", "../hola"); - } - - #[test] - fn string_with_three_ndots_and_chars() { - check_ndots_expansion("a...b", "a...b"); - } - - #[test] - fn string_with_two_ndots_and_chars() { - check_ndots_expansion("a..b", "a..b"); - } - - #[test] - fn string_with_one_dot_and_chars() { - check_ndots_expansion("a.b", "a.b"); - } - - #[test] - fn resolve_dots_double_dots_no_change() { - // Can't resolve this as we don't know our parent dir - assert_eq!(Path::new(".."), resolve_dots(Path::new("..").into())); - } - - #[test] - fn resolve_dots_single_dot_no_change() { - // Can't resolve this as we don't know our current dir - assert_eq!(Path::new("."), resolve_dots(Path::new(".").into())); - } - - #[test] - fn resolve_dots_multi_single_dots_no_change() { - assert_eq!(Path::new("././."), resolve_dots(Path::new("././.").into())); - } - - #[test] - fn resolve_multi_double_dots_no_change() { - assert_eq!( - Path::new("../../../"), - resolve_dots(Path::new("../../../").into()) - ); - } - - #[test] - fn resolve_dots_no_change_with_dirs() { - // Can't resolve this as we don't know our parent dir - assert_eq!( - Path::new("../../../dir1/dir2/"), - resolve_dots(Path::new("../../../dir1/dir2").into()) - ); - } - - #[test] - fn resolve_dots_simple() { - assert_eq!( - Path::new("/foo"), - resolve_dots(Path::new("/foo/bar/..").into()) - ); - } - - #[test] - fn resolve_dots_complex() { - assert_eq!( - Path::new("/test"), - resolve_dots(Path::new("/foo/./bar/../../test/././test2/../").into()) - ); - } - - // Windows tests - #[cfg(windows)] - mod windows { - use super::*; - - #[test] - fn string_with_three_ndots() { - check_ndots_expansion(r"..\..", "..."); - } - - #[test] - fn string_with_mixed_ndots_and_chars() { - check_ndots_expansion( - r"a...b/./c..d/../e.f/..\..\..//.", - "a...b/./c..d/../e.f/....//.", - ); - } - - #[test] - fn string_with_three_ndots_and_final_slash() { - check_ndots_expansion(r"..\../", ".../"); - } - - #[test] - fn string_with_three_ndots_and_garbage() { - check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*["); - } - } - - // non-Windows tests - #[cfg(not(windows))] - mod non_windows { - use super::*; - #[test] - fn string_with_three_ndots() { - check_ndots_expansion(r"../..", "..."); - } - - #[test] - fn string_with_mixed_ndots_and_chars() { - check_ndots_expansion( - "a...b/./c..d/../e.f/../../..//.", - "a...b/./c..d/../e.f/....//.", - ); - } - - #[test] - fn string_with_three_ndots_and_final_slash() { - check_ndots_expansion("../../", ".../"); - } - - #[test] - fn string_with_three_ndots_and_garbage() { - check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*["); - } - } - - mod tilde { - use super::*; - - fn check_expanded(s: &str) { - let home = Path::new("/home"); - let buf = Some(PathBuf::from(home)); - assert!(expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with(&home)); - - // Tests the special case in expand_tilde for "/" as home - let home = Path::new("/"); - let buf = Some(PathBuf::from(home)); - assert!(!expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with("//")); - } - - fn check_not_expanded(s: &str) { - let home = PathBuf::from("/home"); - let expanded = expand_tilde_with(Cow::Borrowed(Path::new(s)), Some(home)); - assert!( - std::matches!(expanded, Cow::Borrowed(_)), - "No PathBuf should be needed here (unecessary allocation)" - ); - assert!(expanded == Path::new(s)); - } - - #[test] - fn string_with_tilde() { - check_expanded("~"); - } - - #[test] - fn string_with_tilde_forward_slash() { - check_expanded("~/test/"); - } - - #[test] - fn string_with_tilde_double_forward_slash() { - check_expanded("~//test/"); - } - - #[test] - fn does_not_expand_tilde_if_tilde_is_not_first_character() { - check_not_expanded("1~1"); - } - - #[cfg(windows)] - #[test] - fn string_with_tilde_backslash() { - check_expanded("~\\test/test2/test3"); - } - - #[cfg(windows)] - #[test] - fn string_with_double_tilde_backslash() { - check_expanded("~\\\\test\\test2/test3"); - } - } -} +mod dots; +mod expansions; +mod tilde; +mod util; + +pub use expansions::{canonicalize, canonicalize_with, expand_path, expand_path_with}; +pub use tilde::expand_tilde; +pub use util::trim_trailing_slash; diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs new file mode 100644 index 000000000..e1c7ec56a --- /dev/null +++ b/crates/nu-path/src/tilde.rs @@ -0,0 +1,85 @@ +use std::path::{Path, PathBuf}; + +fn expand_tilde_with(path: impl AsRef, home: Option) -> PathBuf { + let path = path.as_ref(); + + if !path.starts_with("~") { + return path.into(); + } + + match home { + None => path.into(), + Some(mut h) => { + if h == Path::new("/") { + // Corner case: `h` is a root directory; + // don't prepend extra `/`, just drop the tilde. + path.strip_prefix("~").unwrap_or(path).into() + } else { + if let Ok(p) = path.strip_prefix("~/") { + h.push(p) + } + h + } + } + } +} + +/// Expand tilde ("~") into a home directory if it is the first path component +pub fn expand_tilde(path: impl AsRef) -> PathBuf { + // TODO: Extend this to work with "~user" style of home paths + expand_tilde_with(path, dirs_next::home_dir()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn check_expanded(s: &str) { + let home = Path::new("/home"); + let buf = Some(PathBuf::from(home)); + assert!(expand_tilde_with(Path::new(s), buf).starts_with(&home)); + + // Tests the special case in expand_tilde for "/" as home + let home = Path::new("/"); + let buf = Some(PathBuf::from(home)); + assert!(!expand_tilde_with(Path::new(s), buf).starts_with("//")); + } + + fn check_not_expanded(s: &str) { + let home = PathBuf::from("/home"); + let expanded = expand_tilde_with(Path::new(s), Some(home)); + assert!(expanded == Path::new(s)); + } + + #[test] + fn string_with_tilde() { + check_expanded("~"); + } + + #[test] + fn string_with_tilde_forward_slash() { + check_expanded("~/test/"); + } + + #[test] + fn string_with_tilde_double_forward_slash() { + check_expanded("~//test/"); + } + + #[test] + fn does_not_expand_tilde_if_tilde_is_not_first_character() { + check_not_expanded("1~1"); + } + + #[cfg(windows)] + #[test] + fn string_with_tilde_backslash() { + check_expanded("~\\test/test2/test3"); + } + + #[cfg(windows)] + #[test] + fn string_with_double_tilde_backslash() { + check_expanded("~\\\\test\\test2/test3"); + } +} diff --git a/crates/nu-path/src/util.rs b/crates/nu-path/src/util.rs new file mode 100644 index 000000000..63351e6ae --- /dev/null +++ b/crates/nu-path/src/util.rs @@ -0,0 +1,4 @@ +/// Trim trailing path separator from a string +pub fn trim_trailing_slash(s: &str) -> &str { + s.trim_end_matches(std::path::is_separator) +} diff --git a/crates/nu-path/tests/canonicalize.rs b/crates/nu-path/tests/canonicalize.rs new file mode 100644 index 000000000..2db66c800 --- /dev/null +++ b/crates/nu-path/tests/canonicalize.rs @@ -0,0 +1,412 @@ +use std::path::Path; + +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; + +use nu_path::{canonicalize, canonicalize_with}; + +#[test] +fn canonicalize_path() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let mut spam = dirs.test().clone(); + spam.push("spam.txt"); + + let actual = canonicalize(spam).expect("Failed to canonicalize"); + + assert!(actual.ends_with("spam.txt")); + }); +} + +#[test] +fn canonicalize_unicode_path() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("🚒.txt")]); + + let mut spam = dirs.test().clone(); + spam.push("🚒.txt"); + + let actual = canonicalize(spam).expect("Failed to canonicalize"); + + assert!(actual.ends_with("🚒.txt")); + }); +} + +#[ignore] +#[test] +fn canonicalize_non_utf8_path() { + // TODO +} + +#[test] +fn canonicalize_path_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with("spam.txt", dirs.test()).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_unicode_path_relative_to_unicode_path_with_spaces() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("e-$ èрт🚒♞中片-j"); + sandbox.with_files(vec![EmptyFile("e-$ èрт🚒♞中片-j/🚒.txt")]); + + let mut relative_to = dirs.test().clone(); + relative_to.push("e-$ èрт🚒♞中片-j"); + + let actual = canonicalize_with("🚒.txt", relative_to).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("e-$ èрт🚒♞中片-j/🚒.txt"); + + assert_eq!(actual, expected); + }); +} + +#[ignore] +#[test] +fn canonicalize_non_utf8_path_relative_to_non_utf8_path_with_spaces() { + // TODO +} + +#[test] +fn canonicalize_absolute_path_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let mut absolute_path = dirs.test().clone(); + absolute_path.push("spam.txt"); + + let actual = canonicalize_with(&absolute_path, "non/existent/directory") + .expect("Failed to canonicalize"); + let expected = absolute_path; + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_dot() { + let actual = canonicalize(".").expect("Failed to canonicalize"); + let expected = std::env::current_dir().expect("Could not get current directory"); + + assert_eq!(actual, expected); +} + +#[test] +fn canonicalize_many_dots() { + let actual = canonicalize("././/.//////./././//.///").expect("Failed to canonicalize"); + let expected = std::env::current_dir().expect("Could not get current directory"); + + assert_eq!(actual, expected); +} + +#[test] +fn canonicalize_path_with_dot_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with("./spam.txt", dirs.test()).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_many_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with("././/.//////./././//.////spam.txt", dirs.test()) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_double_dot() { + let actual = canonicalize("..").expect("Failed to canonicalize"); + let cwd = std::env::current_dir().expect("Could not get current directory"); + let expected = cwd + .parent() + .expect("Could not get parent of current directory"); + + assert_eq!(actual, expected); +} + +#[test] +fn canonicalize_path_with_double_dot_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = + canonicalize_with("foo/../spam.txt", dirs.test()).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_many_double_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with("foo/bar/baz/../../../spam.txt", dirs.test()) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_ndots() { + let actual = canonicalize("...").expect("Failed to canonicalize"); + let cwd = std::env::current_dir().expect("Could not get current directory"); + let expected = cwd + .parent() + .expect("Could not get parent of current directory") + .parent() + .expect("Could not get parent of a parent of current directory"); + + assert_eq!(actual, expected); +} + +#[test] +fn canonicalize_path_with_3_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = + canonicalize_with("foo/bar/.../spam.txt", dirs.test()).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_many_3_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with( + "foo/bar/baz/eggs/sausage/bacon/.../.../.../spam.txt", + dirs.test(), + ) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_4_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with("foo/bar/baz/..../spam.txt", dirs.test()) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_many_4_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = canonicalize_with( + "foo/bar/baz/eggs/sausage/bacon/..../..../spam.txt", + dirs.test(), + ) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_path_with_way_too_many_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon/vikings"); + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let mut relative_to = dirs.test().clone(); + relative_to.push("foo/bar/baz/eggs/sausage/bacon/vikings"); + + let actual = canonicalize_with("././..////././...///././.....///spam.txt", relative_to) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_unicode_path_with_way_too_many_dots_relative_to_unicode_path_with_spaces() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä"); + sandbox.with_files(vec![EmptyFile("🚒.txt")]); + + let mut relative_to = dirs.test().clone(); + relative_to.push("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä"); + + let actual = canonicalize_with("././..////././...///././.....///🚒.txt", relative_to) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("🚒.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_tilde() { + let tilde_path = "~"; + + let actual = canonicalize(tilde_path).expect("Failed to canonicalize"); + + assert!(actual.is_absolute()); + assert!(!actual.starts_with("~")); +} + +#[test] +fn canonicalize_tilde_relative_to() { + let tilde_path = "~"; + + let actual = + canonicalize_with(tilde_path, "non/existent/path").expect("Failed to canonicalize"); + + assert!(actual.is_absolute()); + assert!(!actual.starts_with("~")); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn canonicalize_symlink() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + sandbox.symlink("spam.txt", "link_to_spam.txt"); + + let mut symlink_path = dirs.test().clone(); + symlink_path.push("link_to_spam.txt"); + + let actual = canonicalize(symlink_path).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn canonicalize_symlink_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + sandbox.symlink("spam.txt", "link_to_spam.txt"); + + let actual = + canonicalize_with("link_to_spam.txt", dirs.test()).expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(windows))] // seems like Windows symlink requires existing file or dir +#[test] +fn canonicalize_symlink_loop_relative_to_should_fail() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + // sandbox.with_files(vec![EmptyFile("spam.txt")]); + sandbox.symlink("spam.txt", "link_to_spam.txt"); + sandbox.symlink("link_to_spam.txt", "spam.txt"); + + let actual = canonicalize_with("link_to_spam.txt", dirs.test()); + + assert!(actual.is_err()); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn canonicalize_nested_symlink_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + sandbox.symlink("spam.txt", "link_to_spam.txt"); + sandbox.symlink("link_to_spam.txt", "link_to_link_to_spam.txt"); + + let actual = canonicalize_with("link_to_link_to_spam.txt", dirs.test()) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn canonicalize_nested_symlink_within_symlink_dir_relative_to() { + Playground::setup("nu_path_test_1", |dirs, sandbox| { + sandbox.mkdir("foo/bar/baz"); + sandbox.with_files(vec![EmptyFile("foo/bar/baz/spam.txt")]); + sandbox.symlink("foo/bar/baz/spam.txt", "foo/bar/link_to_spam.txt"); + sandbox.symlink("foo/bar/link_to_spam.txt", "foo/link_to_link_to_spam.txt"); + sandbox.symlink("foo", "link_to_foo"); + + let actual = canonicalize_with("link_to_foo/link_to_link_to_spam.txt", dirs.test()) + .expect("Failed to canonicalize"); + let mut expected = dirs.test().clone(); + expected.push("foo/bar/baz/spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn canonicalize_should_fail() { + let path = Path::new("/foo/bar/baz"); // hopefully, this path does not exist + + assert!(canonicalize(path).is_err()); +} + +#[test] +fn canonicalize_with_should_fail() { + let relative_to = "/foo"; + let path = "bar/baz"; + + assert!(canonicalize_with(path, relative_to).is_err()); +} diff --git a/crates/nu-path/tests/expand_path.rs b/crates/nu-path/tests/expand_path.rs new file mode 100644 index 000000000..9453e1300 --- /dev/null +++ b/crates/nu-path/tests/expand_path.rs @@ -0,0 +1,294 @@ +use std::path::PathBuf; + +use nu_test_support::playground::Playground; + +use nu_path::{expand_path, expand_path_with}; + +#[test] +fn expand_path_with_and_without_relative() { + let relative_to = "/foo/bar"; + let path = "../.."; + let full_path = "/foo/bar/../.."; + + assert_eq!(expand_path(full_path), expand_path_with(path, relative_to),); +} + +#[test] +fn expand_path_with_relative() { + let relative_to = "/foo/bar"; + let path = "../.."; + + assert_eq!(PathBuf::from("/"), expand_path_with(path, relative_to),); +} + +#[test] +fn expand_path_no_change() { + let path = "/foo/bar"; + + let actual = expand_path(&path); + + assert_eq!(actual, PathBuf::from(path)); +} + +#[test] +fn expand_unicode_path_no_change() { + Playground::setup("nu_path_test_1", |dirs, _| { + let mut spam = dirs.test().clone(); + spam.push("🚒.txt"); + + let actual = expand_path(spam); + let mut expected = dirs.test().clone(); + expected.push("🚒.txt"); + + assert_eq!(actual, expected); + }); +} + +#[ignore] +#[test] +fn expand_non_utf8_path() { + // TODO +} + +#[test] +fn expand_path_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_unicode_path_relative_to_unicode_path_with_spaces() { + Playground::setup("nu_path_test_1", |dirs, _| { + let mut relative_to = dirs.test().clone(); + relative_to.push("e-$ èрт🚒♞中片-j"); + + let actual = expand_path_with("🚒.txt", relative_to); + let mut expected = dirs.test().clone(); + expected.push("e-$ èрт🚒♞中片-j/🚒.txt"); + + assert_eq!(actual, expected); + }); +} + +#[ignore] +#[test] +fn expand_non_utf8_path_relative_to_non_utf8_path_with_spaces() { + // TODO +} + +#[test] +fn expand_absolute_path_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let mut absolute_path = dirs.test().clone(); + absolute_path.push("spam.txt"); + + let actual = expand_path_with(&absolute_path, "non/existent/directory"); + let expected = absolute_path; + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_dot() { + let actual = expand_path("."); + let expected = PathBuf::from("."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_many_dots() { + let actual = expand_path("././/.//////./././//.///"); + let expected = PathBuf::from("././././././."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_with_dot_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("./spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_double_dot() { + let actual = expand_path(".."); + let expected = PathBuf::from(".."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_dot_double_dot() { + let actual = expand_path("./.."); + let expected = PathBuf::from("./.."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_double_dot_dot() { + let actual = expand_path("../."); + let expected = PathBuf::from(".."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_with_many_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("././/.//////./././//.////spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_double_dot_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("foo/../spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_many_double_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("foo/bar/baz/../../../spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_ndots() { + let actual = expand_path("..."); + let mut expected = PathBuf::from(".."); + expected.push(".."); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_normal_path_ndots() { + let actual = expand_path("foo/bar/baz/..."); + let expected = PathBuf::from("foo"); + + assert_eq!(actual, expected); +} + +#[test] +fn expand_path_with_3_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("foo/bar/.../spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_many_3_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with( + "foo/bar/baz/eggs/sausage/bacon/.../.../.../spam.txt", + dirs.test(), + ); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_4_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with("foo/bar/baz/..../spam.txt", dirs.test()); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_many_4_ndots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let actual = expand_path_with( + "foo/bar/baz/eggs/sausage/bacon/..../..../spam.txt", + dirs.test(), + ); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_with_way_too_many_dots_relative_to() { + Playground::setup("nu_path_test_1", |dirs, _| { + let mut relative_to = dirs.test().clone(); + relative_to.push("foo/bar/baz/eggs/sausage/bacon/vikings"); + + let actual = expand_path_with("././..////././...///././.....///spam.txt", relative_to); + let mut expected = dirs.test().clone(); + expected.push("spam.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_unicode_path_with_way_too_many_dots_relative_to_unicode_path_with_spaces() { + Playground::setup("nu_path_test_1", |dirs, _| { + let mut relative_to = dirs.test().clone(); + relative_to.push("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä"); + + let actual = expand_path_with("././..////././...///././.....///🚒.txt", relative_to); + let mut expected = dirs.test().clone(); + expected.push("🚒.txt"); + + assert_eq!(actual, expected); + }); +} + +#[test] +fn expand_path_tilde() { + let tilde_path = "~"; + + let actual = expand_path(tilde_path); + + assert!(actual.is_absolute()); + assert!(!actual.starts_with("~")); +} + +#[test] +fn expand_path_tilde_relative_to() { + let tilde_path = "~"; + + let actual = expand_path_with(tilde_path, "non/existent/path"); + + assert!(actual.is_absolute()); + assert!(!actual.starts_with("~")); +} diff --git a/crates/nu-path/tests/mod.rs b/crates/nu-path/tests/mod.rs new file mode 100644 index 000000000..6fe847421 --- /dev/null +++ b/crates/nu-path/tests/mod.rs @@ -0,0 +1,3 @@ +mod canonicalize; +mod expand_path; +mod util; diff --git a/crates/nu-path/tests/util.rs b/crates/nu-path/tests/util.rs new file mode 100644 index 000000000..601d9dd43 --- /dev/null +++ b/crates/nu-path/tests/util.rs @@ -0,0 +1,45 @@ +use nu_path::trim_trailing_slash; +use std::path::MAIN_SEPARATOR; + +/// Helper function that joins string literals with '/' or '\', based on the host OS +fn join_path_sep(pieces: &[&str]) -> String { + let sep_string = String::from(MAIN_SEPARATOR); + pieces.join(&sep_string) +} + +#[test] +fn trims_trailing_slash_without_trailing_slash() { + let path = join_path_sep(&["some", "path"]); + + let actual = trim_trailing_slash(&path); + + assert_eq!(actual, &path) +} + +#[test] +fn trims_trailing_slash() { + let path = join_path_sep(&["some", "path", ""]); + + let actual = trim_trailing_slash(&path); + let expected = join_path_sep(&["some", "path"]); + + assert_eq!(actual, &expected) +} + +#[test] +fn trims_many_trailing_slashes() { + let path = join_path_sep(&["some", "path", "", "", "", ""]); + + let actual = trim_trailing_slash(&path); + let expected = join_path_sep(&["some", "path"]); + + assert_eq!(actual, &expected) +} + +#[test] +fn trims_trailing_slash_empty() { + let path = String::from(MAIN_SEPARATOR); + let actual = trim_trailing_slash(&path); + + assert_eq!(actual, "") +} diff --git a/crates/nu-test-support/Cargo.toml b/crates/nu-test-support/Cargo.toml index 076d48e22..2232d98a2 100644 --- a/crates/nu-test-support/Cargo.toml +++ b/crates/nu-test-support/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] nu-errors = { version = "0.36.1", path="../nu-errors" } +nu-path = { version="0.36.1", path="../nu-path" } nu-protocol = { path="../nu-protocol", version = "0.36.1" } nu-source = { path="../nu-source", version = "0.36.1" } nu-value-ext = { version = "0.36.1", path="../nu-value-ext" } diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index d97f984ab..f7f0951a0 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -30,7 +30,7 @@ macro_rules! nu { ); let test_bins = $crate::fs::binaries(); - let test_bins = dunce::canonicalize(&test_bins).unwrap_or_else(|e| { + let test_bins = nu_path::canonicalize(&test_bins).unwrap_or_else(|e| { panic!( "Couldn't canonicalize dummy binaries path {}: {:?}", test_bins.display(), @@ -111,7 +111,7 @@ macro_rules! nu_with_plugins { ); let test_bins = $crate::fs::binaries(); - let test_bins = dunce::canonicalize(&test_bins).unwrap_or_else(|e| { + let test_bins = nu_path::canonicalize(&test_bins).unwrap_or_else(|e| { panic!( "Couldn't canonicalize dummy binaries path {}: {:?}", test_bins.display(), diff --git a/crates/nu-test-support/src/playground/play.rs b/crates/nu-test-support/src/playground/play.rs index df4533e54..e75f02b6b 100644 --- a/crates/nu-test-support/src/playground/play.rs +++ b/crates/nu-test-support/src/playground/play.rs @@ -78,7 +78,7 @@ impl<'a> Playground<'a> { std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory"); let fixtures = fs::fixtures(); - let fixtures = dunce::canonicalize(fixtures.clone()).unwrap_or_else(|e| { + let fixtures = nu_path::canonicalize(fixtures.clone()).unwrap_or_else(|e| { panic!( "Couldn't canonicalize fixtures path {}: {:?}", fixtures.display(), @@ -97,7 +97,7 @@ impl<'a> Playground<'a> { let playground_root = playground.root.path(); - let test = dunce::canonicalize(playground_root.join(topic)).unwrap_or_else(|e| { + let test = nu_path::canonicalize(playground_root.join(topic)).unwrap_or_else(|e| { panic!( "Couldn't canonicalize test path {}: {:?}", playground_root.join(topic).display(), @@ -105,7 +105,7 @@ impl<'a> Playground<'a> { ) }); - let root = dunce::canonicalize(playground_root).unwrap_or_else(|e| { + let root = nu_path::canonicalize(playground_root).unwrap_or_else(|e| { panic!( "Couldn't canonicalize tests root path {}: {:?}", playground_root.display(), diff --git a/crates/nu-test-support/src/playground/tests.rs b/crates/nu-test-support/src/playground/tests.rs index 78627eb8a..4786cccc1 100644 --- a/crates/nu-test-support/src/playground/tests.rs +++ b/crates/nu-test-support/src/playground/tests.rs @@ -6,7 +6,7 @@ use hamcrest2::assert_that; use hamcrest2::prelude::*; fn path(p: &Path) -> PathBuf { - dunce::canonicalize(p) + nu_path::canonicalize(p) .unwrap_or_else(|e| panic!("Couldn't canonicalize path {}: {:?}", p.display(), e)) } diff --git a/tests/shell/environment/nu_env.rs b/tests/shell/environment/nu_env.rs index a59a72ecf..e22baf0a4 100644 --- a/tests/shell/environment/nu_env.rs +++ b/tests/shell/environment/nu_env.rs @@ -20,7 +20,7 @@ fn picks_up_env_keys_when_entering_trusted_directory() { SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -35,7 +35,6 @@ fn picks_up_env_keys_when_entering_trusted_directory() { }) } -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] #[test] #[serial] @@ -114,7 +113,7 @@ fn picks_up_script_vars_when_entering_trusted_directory() { SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -193,7 +192,7 @@ fn leaving_a_trusted_directory_runs_exit_scripts() { SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -222,7 +221,7 @@ fn entry_scripts_are_called_when_revisiting_a_trusted_directory() { SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -255,7 +254,7 @@ fn given_a_trusted_directory_with_entry_scripts_when_entering_a_subdirectory_ent SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -286,7 +285,7 @@ fn given_a_trusted_directory_with_exit_scripts_when_entering_a_subdirectory_exit SCRIPTS, r#"[env] testkey = "testvalue" - + [scriptvars] myscript = "echo myval" "# @@ -433,7 +432,6 @@ fn given_a_hierachy_of_trusted_directories_going_back_restores_overwritten_varia }) } -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] #[test] #[serial] @@ -488,7 +486,6 @@ fn local_config_env_var_present_and_removed_correctly() { }); } -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] #[test] #[serial] @@ -555,7 +552,6 @@ fn local_config_env_var_gets_overwritten() { }); } -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] #[test] #[serial] @@ -604,7 +600,6 @@ fn autoenv_test_entry_scripts() { }); } -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] #[test] #[serial] diff --git a/tests/shell/mod.rs b/tests/shell/mod.rs index a241f0fa4..343922061 100644 --- a/tests/shell/mod.rs +++ b/tests/shell/mod.rs @@ -5,7 +5,6 @@ use nu_test_support::{nu, pipeline}; use hamcrest2::assert_that; use hamcrest2::prelude::*; -#[cfg(feature = "directories-support")] #[cfg(feature = "which-support")] mod environment;