diff --git a/Cargo.lock b/Cargo.lock index 2a396884f76..e43a165752f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,23 +62,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" @@ -114,6 +115,15 @@ version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.6.0" @@ -175,7 +185,7 @@ dependencies = [ "async-lock 3.2.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-lite 2.1.0", "slab", ] @@ -348,6 +358,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "basic-toml" version = "0.1.7" @@ -390,7 +406,7 @@ dependencies = [ "async-channel 2.1.1", "async-lock 3.2.0", "async-task", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", "futures-lite 2.1.0", "piper", @@ -419,15 +435,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "byteorder" -version = "1.5.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" @@ -640,9 +650,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -741,12 +751,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -846,9 +853,9 @@ dependencies = [ [[package]] name = "defer" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "647605a6345d5e89c3950a36a638c56478af9b414c55c6f2477c73b115f9acde" +checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" [[package]] name = "deranged" @@ -859,6 +866,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "diff" version = "0.1.13" @@ -875,6 +893,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "document-features" version = "0.2.8" @@ -907,9 +936,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -1004,9 +1033,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "filetime" @@ -1146,7 +1175,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1810,6 +1839,8 @@ dependencies = [ name = "gix-fs" version = "0.10.2" dependencies = [ + "crossbeam-channel", + "fastrand 2.1.0", "gix-features 0.38.1", "gix-utils 0.1.12", "serde", @@ -1961,6 +1992,7 @@ dependencies = [ "gix-object 0.42.1", "gix-traverse 0.39.0", "gix-utils 0.1.12", + "gix-validate 0.8.4", "hashbrown 0.14.3", "itoa", "libc", @@ -1977,10 +2009,11 @@ version = "0.0.0" dependencies = [ "bstr", "filetime", - "gix", "gix-features 0.38.1", "gix-hash 0.14.2", "gix-index 0.32.1", + "gix-object 0.42.1", + "gix-odb", "gix-testtools", ] @@ -2593,7 +2626,7 @@ version = "0.14.0" dependencies = [ "bstr", "crc", - "fastrand 2.0.1", + "fastrand 2.1.0", "fs_extra", "gix-discover 0.26.0", "gix-fs 0.10.2", @@ -2635,7 +2668,7 @@ version = "0.42.0" dependencies = [ "async-std", "async-trait", - "base64 0.21.5", + "base64 0.22.1", "blocking", "bstr", "curl", @@ -2727,7 +2760,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f82c41937f00e15a1f6cb0b55307f0ca1f77f4407ff2bf440be35aa688c6a3e" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", ] [[package]] @@ -2735,7 +2768,7 @@ name = "gix-utils" version = "0.1.12" dependencies = [ "bstr", - "fastrand 2.0.1", + "fastrand 2.1.0", "unicode-normalization", ] @@ -2791,6 +2824,7 @@ dependencies = [ "gix-index 0.32.1", "gix-object 0.42.1", "gix-path 0.10.7", + "gix-validate 0.8.4", "serde", ] @@ -2932,9 +2966,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown 0.14.3", ] @@ -3193,6 +3227,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -3275,9 +3315,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -3335,11 +3375,17 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] @@ -3491,6 +3537,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.17" @@ -3708,7 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", ] @@ -4015,9 +4067,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ "bitflags 2.4.1", "fallible-iterator", @@ -4157,6 +4209,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -4182,6 +4243,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "security-framework" version = "2.9.2" @@ -4250,23 +4317,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", + "once_cell", "parking_lot", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", @@ -4345,6 +4412,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -4532,7 +4605,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand 2.1.0", "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -4588,13 +4661,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -4610,10 +4684,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -4695,9 +4770,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" @@ -4933,9 +5008,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" @@ -5414,13 +5489,31 @@ checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zip" -version = "0.6.6" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "1b7a5a9285bd4ee13bdeb3f8a4917eb46557e53f270c783849db8bef37b0ad00" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "crossbeam-utils", + "displaydoc", "flate2", + "indexmap", + "thiserror", "time", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index e5e7d42d1d6..4c950e5b68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,22 +42,22 @@ max = ["max-control", "fast", "gitoxide-core-blocking-client", "http-client-curl ## transports as it uses Rust's HTTP implementation. ## ## As fast as possible, with TUI progress, progress line rendering with auto-configuration, all transports available but less mature pure Rust HTTP implementation, all `ein` tools, CLI colors and local-time support, JSON output, regex support for rev-specs. -max-pure = ["max-control", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "http-client-reqwest", "gitoxide-core-blocking-client" ] +max-pure = ["max-control", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "http-client-reqwest", "gitoxide-core-blocking-client"] ## Like `max`, but with more control for configuration. See the *Package Maintainers* headline for more information. -max-control = ["tracing", "fast-safe", "pretty-cli", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "prodash-render-line", "prodash-render-tui", "prodash/render-line-autoconfigure", "gix/revparse-regex" ] +max-control = ["tracing", "fast-safe", "pretty-cli", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "prodash-render-line", "prodash-render-tui", "prodash/render-line-autoconfigure", "gix/revparse-regex"] ## All of the good stuff, with less fanciness for smaller binaries. ## ## As fast as possible, progress line rendering, all transports based on their most mature implementation (HTTP), all `ein` tools, CLI colors and local-time support, JSON output. -lean = ["fast", "tracing", "pretty-cli", "http-client-curl", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "gitoxide-core-blocking-client", "prodash-render-line" ] +lean = ["fast", "tracing", "pretty-cli", "http-client-curl", "gitoxide-core-tools-query", "gitoxide-core-tools-corpus", "gitoxide-core-tools", "gitoxide-core-blocking-client", "prodash-render-line"] ## The smallest possible build, best suitable for small single-core machines. ## ## This build is essentially limited to local operations without any fanciness. ## ## Optimized for size, no parallelism thus much slower, progress line rendering. -small = ["pretty-cli", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "prodash-render-line", "is-terminal" ] +small = ["pretty-cli", "gix-features/rustsha1", "gix-features/zlib-rust-backend", "prodash-render-line", "is-terminal"] ## Like lean, but uses Rusts async implementations for networking. ## @@ -107,12 +107,12 @@ fast = ["gix/max-performance", "gix/comfort"] fast-safe = ["gix/max-performance-safe", "gix/comfort"] ## Enable tracing in `gitoxide-core`. -tracing = ["dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "gix-features/tracing", "gix-features/tracing-detail" ] +tracing = ["dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "gix-features/tracing", "gix-features/tracing-detail"] ## Use `clap` 3.0 to build the prettiest, best documented and most user-friendly CLI at the expense of binary size. ## Provides a terminal user interface for detailed and exhaustive progress. ## Provides a line renderer for leaner progress display, without the need for a full-blown TUI. -pretty-cli = [ "gitoxide-core/serde", "prodash/progress-tree", "prodash/progress-tree-log", "prodash/local-time", "env_logger/humantime", "env_logger/color", "env_logger/auto-color" ] +pretty-cli = ["gitoxide-core/serde", "prodash/progress-tree", "prodash/progress-tree-log", "prodash/local-time", "env_logger/humantime", "env_logger/color", "env_logger/auto-color"] ## The `--verbose` flag will be powered by an interactive progress mechanism that doubles as log as well as interactive progress ## that appears after a short duration. @@ -285,9 +285,7 @@ members = [ "gix-worktree-stream", "gix-revwalk", "gix-fsck", - "tests/tools", - "gix-diff/tests", "gix-pack/tests", "gix-odb/tests", diff --git a/deny.toml b/deny.toml index 1e8ed3f49e0..01d4cb436f6 100644 --- a/deny.toml +++ b/deny.toml @@ -8,20 +8,6 @@ # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] -# The path where the advisory database is cloned/fetched into -db-path = "~/.cargo/advisory-db" -# The url(s) of the advisory databases to use -db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" -# The lint level for crates that have been yanked from their source registry -yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" ignore = [ # this is `rustls@0.20.9` coming in with `curl`, which doesn't have an update yet. It's only active optionally, not by default. "RUSTSEC-2024-0336", @@ -33,14 +19,13 @@ ignore = [ # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ "Apache-2.0", "BSD-3-Clause", + "BSL-1.0", "MIT", "MIT-0", "ISC", @@ -48,13 +33,6 @@ allow = [ "LicenseRef-ring", "Zlib" ] -# Lint level for licenses considered copyleft -copyleft = "allow" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index aa53e19e5c9..3cc9a67fbf0 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -23,7 +23,7 @@ estimate-hours = ["dep:fs-err", "dep:crossbeam-channel", "dep:smallvec"] query = ["dep:rusqlite"] ## Run algorithms on a corpus of repositories and store their results for later comparison and intelligence gathering. ## *Note that* `organize` we need for finding git repositories fast. -corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "tracing", "dep:parking_lot" ] +corpus = ["dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "tracing", "dep:parking_lot"] ## The ability to create archives from virtual worktrees, similar to `git archive`. archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"] @@ -77,7 +77,7 @@ crossbeam-channel = { version = "0.5.6", optional = true } smallvec = { version = "1.10.0", optional = true } # for 'query' and 'corpus' -rusqlite = { version = "0.30.0", optional = true, features = ["bundled"] } +rusqlite = { version = "0.31.0", optional = true, features = ["bundled"] } # for 'corpus' parking_lot = { version = "0.12.1", optional = true } diff --git a/gitoxide-core/src/lib.rs b/gitoxide-core/src/lib.rs index 2cf788a994a..9ff7ab73760 100644 --- a/gitoxide-core/src/lib.rs +++ b/gitoxide-core/src/lib.rs @@ -84,3 +84,11 @@ pub use discover::discover; #[cfg(all(feature = "async-client", feature = "blocking-client"))] compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive"); + +fn is_dir_to_mode(is_dir: bool) -> gix::index::entry::Mode { + if is_dir { + gix::index::entry::Mode::DIR + } else { + gix::index::entry::Mode::FILE + } +} diff --git a/gitoxide-core/src/repository/attributes/query.rs b/gitoxide-core/src/repository/attributes/query.rs index b3443cdec95..9081eb9ed78 100644 --- a/gitoxide-core/src/repository/attributes/query.rs +++ b/gitoxide-core/src/repository/attributes/query.rs @@ -14,6 +14,7 @@ pub(crate) mod function { use gix::bstr::BStr; use crate::{ + is_dir_to_mode, repository::{ attributes::query::{attributes_cache, Options}, PathsOrPatterns, @@ -38,12 +39,12 @@ pub(crate) mod function { match input { PathsOrPatterns::Paths(paths) => { for path in paths { - let is_dir = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) .metadata() .ok() - .map(|m| m.is_dir()); + .map(|m| is_dir_to_mode(m.is_dir())); - let entry = cache.at_entry(path.as_slice(), is_dir)?; + let entry = cache.at_entry(path.as_slice(), mode)?; if !entry.matching_attributes(&mut matches) { continue; } @@ -61,9 +62,9 @@ pub(crate) mod function { )?; let mut pathspec_matched_entry = false; if let Some(it) = pathspec.index_entries_with_paths(&index) { - for (path, _entry) in it { + for (path, entry) in it { pathspec_matched_entry = true; - let entry = cache.at_entry(path, Some(false))?; + let entry = cache.at_entry(path, entry.mode.into())?; if !entry.matching_attributes(&mut matches) { continue; } @@ -87,10 +88,10 @@ pub(crate) mod function { let path = pattern.path(); let entry = cache.at_entry( path, - Some( + Some(is_dir_to_mode( workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir()) || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), - ), + )), )?; if !entry.matching_attributes(&mut matches) { continue; diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index a5571d45e86..77eeb258a49 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -192,7 +192,7 @@ pub(crate) mod function { ); for (rela_path, baseline) in rx_base { - let entry = cache.at_entry(rela_path.as_str(), Some(false))?; + let entry = cache.at_entry(rela_path.as_str(), None)?; match baseline { Baseline::Attribute { assignments: expected } => { entry.matching_attributes(&mut matches); diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs index ac837fe0303..a0cd212d08e 100644 --- a/gitoxide-core/src/repository/exclude.rs +++ b/gitoxide-core/src/repository/exclude.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, io}; use anyhow::bail; use gix::bstr::BStr; -use crate::{repository::PathsOrPatterns, OutputFormat}; +use crate::{is_dir_to_mode, repository::PathsOrPatterns, OutputFormat}; pub mod query { use std::ffi::OsString; @@ -44,11 +44,11 @@ pub fn query( match input { PathsOrPatterns::Paths(paths) => { for path in paths { - let is_dir = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) .metadata() .ok() - .map(|m| m.is_dir()); - let entry = cache.at_entry(path.as_slice(), is_dir)?; + .map(|m| is_dir_to_mode(m.is_dir())); + let entry = cache.at_entry(path.as_slice(), mode)?; let match_ = entry .matching_exclude_pattern() .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); @@ -66,9 +66,9 @@ pub fn query( )?; if let Some(it) = pathspec.index_entries_with_paths(&index) { - for (path, _entry) in it { + for (path, entry) in it { pathspec_matched_something = true; - let entry = cache.at_entry(path, Some(false))?; + let entry = cache.at_entry(path, entry.mode.into())?; let match_ = entry .matching_exclude_pattern() .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m)); @@ -92,10 +92,10 @@ pub fn query( let path = pattern.path(); let entry = cache.at_entry( path, - Some( + Some(is_dir_to_mode( workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir()) || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), - ), + )), )?; let match_ = entry .matching_exclude_pattern() diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 3f17d55b3c3..4485bad5c3e 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -31,6 +31,7 @@ pub(crate) mod function { }; use crate::{ + is_dir_to_mode, repository::index::entries::{Attributes, Options}, OutputFormat, }; @@ -174,7 +175,7 @@ pub(crate) mod function { } // The user doesn't want attributes, so we set the cache position on demand only None => cache - .at_entry(rela_path, Some(is_dir)) + .at_entry(rela_path, Some(is_dir_to_mode(is_dir))) .ok() .map(|platform| platform.matching_attributes(out)) .unwrap_or_default(), diff --git a/gitoxide-core/src/repository/revision/resolve.rs b/gitoxide-core/src/repository/revision/resolve.rs index b4494e9e046..6e8a2b1ad65 100644 --- a/gitoxide-core/src/repository/revision/resolve.rs +++ b/gitoxide-core/src/repository/revision/resolve.rs @@ -127,11 +127,10 @@ pub(crate) mod function { } gix::object::Kind::Blob if cache.is_some() && spec.path_and_mode().is_some() => { let (path, mode) = spec.path_and_mode().expect("is present"); - let is_dir = Some(mode.is_tree()); match cache.expect("is some") { (BlobFormat::Git, _) => unreachable!("no need for a cache when querying object db"), (BlobFormat::Worktree, cache) => { - let platform = cache.attr_stack.at_entry(path, is_dir, &repo.objects)?; + let platform = cache.attr_stack.at_entry(path, Some(mode.into()), &repo.objects)?; let object = id.object()?; let mut converted = cache.filter.worktree_filter.convert_to_worktree( &object.data, diff --git a/gix-archive/Cargo.toml b/gix-archive/Cargo.toml index 81477eafc07..c08fc5a8264 100644 --- a/gix-archive/Cargo.toml +++ b/gix-archive/Cargo.toml @@ -31,7 +31,7 @@ gix-path = { version = "^0.10.7", path = "../gix-path", optional = true } gix-date = { version = "^0.8.6", path = "../gix-date" } flate2 = { version = "1.0.26", optional = true } -zip = { version = "0.6.6", optional = true, default-features = false, features = ["deflate", "time"] } +zip = { version = "1.3.1", optional = true, default-features = false, features = ["deflate", "time"] } time = { version = "0.3.23", optional = true, default-features = false, features = ["std"] } thiserror = "1.0.26" @@ -42,13 +42,13 @@ tar = { version = "0.4.38", optional = true } document-features = { version = "0.2.0", optional = true } [dev-dependencies] -gix-testtools = { path = "../tests/tools"} -gix-odb = { path = "../gix-odb"} -gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"]} -gix-hash = { path = "../gix-hash"} -gix-attributes = { path = "../gix-attributes"} -gix-object = { path = "../gix-object"} -gix-filter = { path = "../gix-filter"} +gix-testtools = { path = "../tests/tools" } +gix-odb = { path = "../gix-odb" } +gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"] } +gix-hash = { path = "../gix-hash" } +gix-attributes = { path = "../gix-attributes" } +gix-object = { path = "../gix-object" } +gix-filter = { path = "../gix-filter" } [package.metadata.docs.rs] all-features = true diff --git a/gix-archive/src/write.rs b/gix-archive/src/write.rs index 5c5dc885eca..244b40554e0 100644 --- a/gix-archive/src/write.rs +++ b/gix-archive/src/write.rs @@ -126,7 +126,7 @@ where NextFn: FnMut(&mut Stream) -> Result>, gix_worktree_stream::entry::Error>, { let compression_level = match opts.format { - Format::Zip { compression_level } => compression_level.map(|lvl| lvl as i32), + Format::Zip { compression_level } => compression_level.map(|lvl| lvl as i64), _other => return write_stream(stream, next_entry, out, opts), }; @@ -161,10 +161,10 @@ fn append_zip_entry( mut entry: gix_worktree_stream::Entry<'_>, buf: &mut Vec, mtime: zip::DateTime, - compression_level: Option, + compression_level: Option, tree_prefix: Option<&bstr::BString>, ) -> Result<(), Error> { - let file_opts = zip::write::FileOptions::default() + let file_opts = zip::write::FileOptions::<'_, ()>::default() .compression_method(zip::CompressionMethod::Deflated) .compression_level(compression_level) .large_file(entry.bytes_remaining().map_or(true, |len| len > u32::MAX as usize)) diff --git a/gix-archive/tests/archive.rs b/gix-archive/tests/archive.rs index c9a4e8bb281..125d3cd2826 100644 --- a/gix-archive/tests/archive.rs +++ b/gix-archive/tests/archive.rs @@ -208,7 +208,11 @@ mod from_tree { ); let mut link = ar.by_name("prefix/symlink-to-a")?; assert!(!link.is_dir()); - assert!(link.is_file(), "no symlink differentiation"); + assert_eq!( + link.is_symlink(), + cfg!(not(windows)), + "symlinks are supported as well, but only on Unix" + ); assert_eq!( link.unix_mode(), Some(if cfg!(windows) { 0o100644 } else { 0o120644 }), @@ -233,7 +237,7 @@ mod from_tree { noop_pipeline(), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, diff --git a/gix-config/tests/Cargo.toml b/gix-config/tests/Cargo.toml index 992589ad820..0448769115c 100644 --- a/gix-config/tests/Cargo.toml +++ b/gix-config/tests/Cargo.toml @@ -19,13 +19,13 @@ name = "mem" path = "mem.rs" [dev-dependencies] -gix-config = { path = ".."} -gix-testtools = { path = "../../tests/tools"} +gix-config = { path = ".." } +gix-testtools = { path = "../../tests/tools" } gix = { path = "../../gix", default-features = false } gix-ref = { path = "../../gix-ref" } gix-path = { path = "../../gix-path" } gix-sec = { path = "../../gix-sec" } -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } bstr = { version = "1.3.0", default-features = false, features = ["std"] } bytesize = "1.3.0" diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 091c5a9cf3c..41b4cc928ea 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -583,14 +583,14 @@ impl Platform { if self.diff_cache.contains_key(storage) { return Ok(()); } - let entry = self - .attr_stack - .at_entry(rela_path, Some(false), objects) - .map_err(|err| set_resource::Error::Attributes { - source: err, - kind, - rela_path: rela_path.to_owned(), - })?; + let entry = + self.attr_stack + .at_entry(rela_path, None, objects) + .map_err(|err| set_resource::Error::Attributes { + source: err, + kind, + rela_path: rela_path.to_owned(), + })?; let mut buf = Vec::new(); let out = self.filter.convert_to_diffable( &id, diff --git a/gix-diff/tests/blob/pipeline.rs b/gix-diff/tests/blob/pipeline.rs index fb1ef355715..f4cfeba5d1b 100644 --- a/gix-diff/tests/blob/pipeline.rs +++ b/gix-diff/tests/blob/pipeline.rs @@ -507,7 +507,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(out.data, Some(pipeline::Data::Binary { size: 11 })); assert_eq!(buf.len(), 0, "buffers are cleared even if we read them"); - let platform = attributes.at_entry("c", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("c", None, &gix_object::find::Never)?; let id = db.insert("b"); let out = filter.convert_to_diffable( @@ -589,7 +589,7 @@ pub(crate) mod convert_to_diffable { let mut db = ObjectDb::default(); let null = gix_hash::Kind::Sha1.null(); let mut buf = Vec::new(); - let platform = attributes.at_entry("a", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("a", None, &gix_object::find::Never)?; let worktree_modes = [ pipeline::Mode::ToWorktreeAndBinaryToText, pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, @@ -672,7 +672,7 @@ pub(crate) mod convert_to_diffable { "no filter was applied in this mode, also when using the ODB" ); - let platform = attributes.at_entry("missing", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("missing", None, &gix_object::find::Never)?; for mode in all_modes { buf.push(1); let out = filter.convert_to_diffable( @@ -731,7 +731,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("b", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("b", None, &gix_object::find::Never)?; for mode in all_modes { buf.push(1); let out = filter.convert_to_diffable( @@ -781,7 +781,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(buf.len(), 0, "it's always cleared before any potential use"); } - let platform = attributes.at_entry("c", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("c", None, &gix_object::find::Never)?; for mode in worktree_modes { let out = filter.convert_to_diffable( &null, @@ -827,7 +827,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("unset", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("unset", None, &gix_object::find::Never)?; for mode in all_modes { let out = filter.convert_to_diffable( &null, @@ -879,7 +879,7 @@ pub(crate) mod convert_to_diffable { assert_eq!(buf.len(), 0); } - let platform = attributes.at_entry("d", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("d", None, &gix_object::find::Never)?; let id = db.insert("d-in-db"); for mode in worktree_modes { let out = filter.convert_to_diffable( @@ -923,7 +923,7 @@ pub(crate) mod convert_to_diffable { ); } - let platform = attributes.at_entry("e-no-attr", Some(false), &gix_object::find::Never)?; + let platform = attributes.at_entry("e-no-attr", None, &gix_object::find::Never)?; let out = filter.convert_to_diffable( &null, EntryKind::Blob, diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index a57319535ba..9fb2b54a5a6 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -161,7 +161,11 @@ pub fn path( .as_mut() .map_or(Ok(None), |stack| { stack - .at_entry(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()), ctx.objects) + .at_entry( + rela_path.as_bstr(), + disk_kind.map(|ft| is_dir_to_mode(ft.is_dir())), + ctx.objects, + ) .map(|platform| platform.excluded_kind()) }) .map_err(Error::ExcludesAccess)? @@ -203,9 +207,9 @@ pub fn path( && ctx.excludes.is_some() && kind.map_or(false, |ft| ft == entry::Kind::Symlink) { - path.metadata().ok().map(|md| md.is_dir()).or(Some(false)) + path.metadata().ok().map(|md| is_dir_to_mode(md.is_dir())) } else { - kind.map(|ft| ft.is_dir()) + kind.map(|ft| is_dir_to_mode(ft.is_dir())) }; let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| { @@ -408,3 +412,11 @@ fn is_eq(lhs: &BStr, rhs: impl AsRef, ignore_case: bool) -> bool { lhs == rhs.as_ref() } } + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix-discover/Cargo.toml b/gix-discover/Cargo.toml index 43c5ad26f63..a474d56e652 100644 --- a/gix-discover/Cargo.toml +++ b/gix-discover/Cargo.toml @@ -27,11 +27,11 @@ dunce = "1.0.3" [dev-dependencies] gix-testtools = { path = "../tests/tools" } -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } is_ci = "1.1.1" [target.'cfg(target_os = "macos")'.dev-dependencies] -defer = "0.1.0" +defer = "0.2.1" [target.'cfg(any(unix, windows))'.dev-dependencies] tempfile = "3.2.0" diff --git a/gix-discover/src/is.rs b/gix-discover/src/is.rs index 055a68e67cd..c0172ef9a06 100644 --- a/gix-discover/src/is.rs +++ b/gix-discover/src/is.rs @@ -172,13 +172,7 @@ pub(crate) fn git_with_metadata( // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here. // With ref-table, the has is probably stored as part of the ref-db itself, so we can handle it from there. // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly. - let object_hash_should_not_matter_here = gix_hash::Kind::Sha1; - let refs = gix_ref::file::Store::at( - dot_git.as_ref().into(), - gix_ref::store::WriteReflog::Normal, - object_hash_should_not_matter_here, - false, - ); + let refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default()); let head = refs.find_loose("HEAD")?; if head.name.as_bstr() != "HEAD" { return Err(crate::is_git::Error::MisplacedHead { diff --git a/gix-features/Cargo.toml b/gix-features/Cargo.toml index 3fc3b20df72..7302c9fbef1 100644 --- a/gix-features/Cargo.toml +++ b/gix-features/Cargo.toml @@ -25,7 +25,7 @@ progress-unit-human-numbers = ["prodash?/unit-human"] progress-unit-bytes = ["dep:bytesize", "prodash?/unit-bytes"] ## If set, walkdir iterators will be multi-threaded. -fs-walkdir-parallel = [ "dep:jwalk", "dep:gix-utils" ] +fs-walkdir-parallel = ["dep:jwalk", "dep:gix-utils"] ## Provide utilities suitable for working with the `std::fs::read_dir()`. fs-read-dir = ["dep:gix-utils"] @@ -34,10 +34,10 @@ fs-read-dir = ["dep:gix-utils"] ## ## Note that this may have overhead as well, thus instrumentations should be used stategically, only providing coarse tracing by default and adding details ## only where needed while marking them with the appropriate level. -tracing = [ "gix-trace/tracing" ] +tracing = ["gix-trace/tracing"] ## If enabled, detailed tracing is also emitted, which can greatly increase insights but at a cost. -tracing-detail = [ "gix-trace/tracing-detail" ] +tracing-detail = ["gix-trace/tracing-detail"] ## Use scoped threads and channels to parallelize common workloads on multiple objects. If enabled, it is used everywhere ## where it makes sense. @@ -45,7 +45,7 @@ tracing-detail = [ "gix-trace/tracing-detail" ] ## The `threading` module will contain thread-safe primitives for shared ownership and mutation, otherwise these will be their single threaded counterparts. ## This way, single-threaded applications don't have to pay for threaded primitives. parallel = ["dep:crossbeam-channel", - "dep:parking_lot"] + "dep:parking_lot"] ## If enabled, OnceCell will be made available for interior mutability either in sync or unsync forms. once_cell = ["dep:once_cell"] ## Makes facilities of the `walkdir` crate partially available. @@ -159,7 +159,7 @@ bstr = { version = "1.3.0", default-features = false } # Assembly doesn't yet compile on MSVC on windows, but does on GNU, see https://github.com/RustCrypto/asm-hashes/issues/17 # At this time, only aarch64, x86 and x86_64 are supported. -[target.'cfg(all(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), not(target_env = "msvc")))'.dependencies] +[target.'cfg(all(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), not(target_os = "windows")))'.dependencies] sha1 = { version = "0.10.0", optional = true, features = ["asm"] } [package.metadata.docs.rs] diff --git a/gix-filter/Cargo.toml b/gix-filter/Cargo.toml index dfaa2d85115..766dbadc209 100644 --- a/gix-filter/Cargo.toml +++ b/gix-filter/Cargo.toml @@ -30,6 +30,6 @@ smallvec = "1.10.0" [dev-dependencies] -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } gix-testtools = { path = "../tests/tools" } gix-worktree = { path = "../gix-worktree", default-features = false, features = ["attributes"] } diff --git a/gix-filter/tests/pipeline/convert_to_git.rs b/gix-filter/tests/pipeline/convert_to_git.rs index 79d79993238..5cff5c888e3 100644 --- a/gix-filter/tests/pipeline/convert_to_git.rs +++ b/gix-filter/tests/pipeline/convert_to_git.rs @@ -53,7 +53,7 @@ fn all_stages_mean_streaming_is_impossible() -> gix_testtools::Result { Path::new("any.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -82,7 +82,7 @@ fn only_driver_means_streaming_is_possible() -> gix_testtools::Result { Path::new("subdir/doesnot/matter/any.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -112,7 +112,7 @@ fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::Result { Path::new("other.txt"), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, diff --git a/gix-filter/tests/pipeline/convert_to_worktree.rs b/gix-filter/tests/pipeline/convert_to_worktree.rs index 2449c50304a..be757e01ab8 100644 --- a/gix-filter/tests/pipeline/convert_to_worktree.rs +++ b/gix-filter/tests/pipeline/convert_to_worktree.rs @@ -21,7 +21,7 @@ fn all_stages() -> gix_testtools::Result { "any.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -54,7 +54,7 @@ fn all_stages_no_filter() -> gix_testtools::Result { "other.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, @@ -86,7 +86,7 @@ fn no_filter() -> gix_testtools::Result { "other.txt".into(), &mut |path, attrs| { cache - .at_entry(path, Some(false), &gix_object::find::Never) + .at_entry(path, None, &gix_object::find::Never) .expect("cannot fail") .matching_attributes(attrs); }, diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml index ebdd3993a24..447c9787ffa 100644 --- a/gix-fs/Cargo.toml +++ b/gix-fs/Cargo.toml @@ -21,5 +21,9 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = ["fs- gix-utils = { version = "^0.1.12", path = "../gix-utils" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } +# For `Capabilities` to assure parallel operation works. +fastrand = { version = "2.1.0", default-features = false, features = ["std"] } + [dev-dependencies] tempfile = "3.5.0" +crossbeam-channel = "0.5.0" diff --git a/gix-fs/src/capabilities.rs b/gix-fs/src/capabilities.rs index 3a384a26dca..4fa6892eed7 100644 --- a/gix-fs/src/capabilities.rs +++ b/gix-fs/src/capabilities.rs @@ -60,7 +60,8 @@ impl Capabilities { use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; // test it exactly as we typically create executable files, not using chmod. - let test_path = root.join("_test_executable_bit"); + let rand = fastrand::usize(..); + let test_path = root.join(format!("_test_executable_bit{rand}")); let res = std::fs::OpenOptions::new() .create_new(true) .write(true) @@ -87,8 +88,9 @@ impl Capabilities { } fn probe_precompose_unicode(root: &Path) -> std::io::Result { - let precomposed = "ä"; - let decomposed = "a\u{308}"; + let rand = fastrand::usize(..); + let precomposed = format!("ä{rand}"); + let decomposed = format!("a\u{308}{rand}"); let precomposed = root.join(precomposed); std::fs::OpenOptions::new() @@ -101,7 +103,8 @@ impl Capabilities { } fn probe_symlink(root: &Path) -> std::io::Result { - let link_path = root.join("__file_link"); + let rand = fastrand::usize(..); + let link_path = root.join(format!("__file_link{rand}")); if crate::symlink::create("dangling".as_ref(), &link_path).is_err() { return Ok(false); } diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 5d3dfeccd34..c5cf73ca459 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use crate::Stack; @@ -22,20 +22,22 @@ impl Stack { /// A delegate for use in a [`Stack`]. pub trait Delegate { - /// Called whenever we push a directory on top of the stack, after the fact. + /// Called whenever we push a directory on top of the stack, and after the respective call to [`push()`](Self::push). /// - /// It is also called if the currently acted on path is a directory in itself. - /// Use `stack.current()` to see the directory. + /// It is only called if the currently acted on path is a directory in itself, which is determined by knowing + /// that it's not the last component of the path. + /// Use [`Stack::current()`] to see the directory. fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; - /// Called after any component was pushed, with the path available at `stack.current()`. + /// Called after any component was pushed, with the path available at [`Stack::current()`]. /// - /// `is_last_component` is true if the path is completely built. + /// `is_last_component` is `true` if the path is completely built, which typically means it's not a directory. fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; /// Called right after a directory-component was popped off the stack. /// - /// Use it to pop information off internal data structures. + /// Use it to pop information off internal data structures. Note that no equivalent call exists for popping + /// the file-component. fn pop_directory(&mut self); } @@ -58,12 +60,15 @@ impl Stack { /// The full path to `relative` will be returned along with the data returned by `push_comp`. /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if /// `relative` paths are terminal, so point to their designated file or directory. + /// The path is also expected to be normalized, and should not contain extra separators, and must not contain `..` + /// or have leading or trailing slashes (or additionally backslashes on Windows). pub fn make_relative_path_current(&mut self, relative: &Path, delegate: &mut dyn Delegate) -> std::io::Result<()> { - debug_assert!( - relative.is_relative(), - "only index paths are handled correctly here, must be relative" - ); - + if self.valid_components != 0 && relative.as_os_str().is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "empty inputs are not allowed", + )); + } if self.valid_components == 0 { delegate.push_directory(self)?; } @@ -95,6 +100,15 @@ impl Stack { } while let Some(comp) = components.next() { + if !matches!(comp, Component::Normal(_)) { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Input path \"{}\" contains relative or absolute components", + relative.display() + ), + )); + } let is_last_component = components.peek().is_none(); self.current_is_directory = !is_last_component; self.current.push(comp); diff --git a/gix-fs/tests/capabilities/mod.rs b/gix-fs/tests/capabilities/mod.rs index 42e5255d423..749bfb3a61f 100644 --- a/gix-fs/tests/capabilities/mod.rs +++ b/gix-fs/tests/capabilities/mod.rs @@ -20,3 +20,29 @@ fn probe() { assert!(caps.executable_bit, "Unix should always honor executable bits"); } } + +#[test] +fn parallel_probe() { + let dir = tempfile::tempdir().unwrap(); + std::fs::File::create(dir.path().join("config")).unwrap(); + let baseline = gix_fs::Capabilities::probe(dir.path()); + + let (tx, rx) = crossbeam_channel::unbounded::<()>(); + let threads: Vec<_> = (0..10) + .map(|_id| { + std::thread::spawn({ + let dir = dir.path().to_owned(); + let rx = rx.clone(); + move || { + for _ in rx {} + let actual = gix_fs::Capabilities::probe(&dir); + assert_eq!(actual, baseline); + } + }) + }) + .collect(); + drop((rx, tx)); + for thread in threads { + thread.join().expect("no panic"); + } +} diff --git a/gix-fs/tests/stack/mod.rs b/gix-fs/tests/stack/mod.rs index 5e122cdb007..c79db87dbf9 100644 --- a/gix-fs/tests/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::join_absolute_paths)] use std::path::{Path, PathBuf}; use gix_fs::Stack; @@ -26,6 +27,287 @@ impl gix_fs::stack::Delegate for Record { } } +fn p(s: &str) -> &Path { + s.as_ref() +} + +/// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. +#[test] +#[cfg(windows)] +fn path_join_handling() { + let absolute = p("/absolute"); + assert!( + absolute.is_relative(), + "on Windows, absolute Linux paths are considered relative (and relative to the current drive)" + ); + let bs_absolute = p("\\absolute"); + assert!( + absolute.is_relative(), + "on Windows, strange single-backslash paths are relative (and relative to the current drive)" + ); + assert_eq!( + p("relative").join(absolute), + absolute, + "relative + absolute = absolute - however, they kind of act like they are absolute in conjunction with relative base paths" + ); + assert_eq!( + p("relative").join(bs_absolute), + bs_absolute, + "relative + absolute = absolute - backslashes aren't special here, and it just acts like it's absolute" + ); + + assert_eq!( + p("c:").join("relative"), + p("c:relative"), + "drive + relative = strange joined result with missing backslash, but it's a valid path that works just like `c:\relative`" + ); + assert_eq!( + p("c:\\").join("relative"), + p("c:\\relative"), + "absolute + relative = joined result" + ); + + assert_eq!( + p("\\\\?\\base").join(absolute), + p("\\\\?\\base\\absolute"), + "absolute1 + unix-absolute2 = joined result with backslash" + ); + assert_eq!( + p("\\\\.\\base").join(absolute), + p("\\\\.\\base\\absolute"), + "absolute1 + absolute2 = joined result with backslash (device namespace)" + ); + assert_eq!( + p("\\\\?\\base").join(bs_absolute), + p("\\\\?\\base\\absolute"), + "absolute1 + absolute2 = joined result" + ); + assert_eq!( + p("\\\\.\\base").join(bs_absolute), + p("\\\\.\\base\\absolute"), + "absolute1 + absolute2 = joined result (device namespace)" + ); + + assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive"); + assert_eq!( + p("d:/").join("C:"), + p("C:"), + "d-drive + c-drive = c-drive - interesting, as C: is supposed to be relative" + ); + assert_eq!( + p("d:\\").join("C:\\"), + p("C:\\"), + "d-drive-with-bs + c-drive-with-bs = c-drive-with-bs - nothing special happens with backslashes" + ); + assert_eq!( + p("c:\\").join("\\\\.\\"), + p("\\\\.\\"), + "c-drive-with-bs + device-namespace-unc = device-namespace-unc" + ); + assert_eq!( + p("/").join("C:/"), + p("C:\\"), + "unix-absolute + win-drive = win-drive, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" + ); + assert_eq!(p("/").join("C:\\"), p("C:\\"), "unix-absolute + win-drive = win-drive"); + assert_eq!( + p("\\\\.").join("C:"), + p("C:"), + "device-namespace-unc + win-drive-relative = win-drive-relative - c: was supposed to be relative, but it's not acting like it." + ); + assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive"); + + assert_eq!( + p("/").join("\\\\localhost"), + p("\\localhost"), + "unix-absolute + win-absolute-unc = win-absolute-unc" + ); + assert_eq!( + p("relative").join("\\\\localhost"), + p("\\\\localhost"), + "relative + win-absolute-unc = win-absolute-unc" + ); +} + +/// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. +#[test] +#[cfg(not(windows))] +fn path_join_handling() { + assert_eq!( + p("relative").join("/absolute"), + p("/absolute"), + "relative + absolute = absolute" + ); + + assert_eq!( + p("/").join("relative"), + p("/relative"), + "absolute + relative = joined result" + ); + + assert_eq!( + p("/").join("/absolute"), + p("/absolute"), + "absolute1 + absolute2 = absolute2" + ); + + assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-drive = joined result"); + assert_eq!(p("/").join("C:/"), p("/C:/"), "absolute + win-absolute = joined result"); + assert_eq!( + p("/").join("C:\\"), + p("/C:\\"), + "absolute + win-absolute = joined result" + ); + assert_eq!( + p("relative").join("C:"), + p("relative/C:"), + "relative + win-drive = joined result" + ); + + assert_eq!( + p("/").join("\\localhost"), + p("/\\localhost"), + "absolute + win-absolute-unc = joined result" + ); + assert_eq!( + p("relative").join("\\localhost"), + p("relative/\\localhost"), + "relative + win-absolute-unc = joined result" + ); +} + +#[test] +fn empty_paths_are_noop_if_no_path_was_pushed_before() { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + s.make_relative_path_current("".as_ref(), &mut r).unwrap(); + assert_eq!( + s.current_relative().to_string_lossy(), + "", + "it's fine to push an empty path to get a value for the stack root, once" + ); +} + +#[test] +fn relative_components_are_invalid() { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + let err = s.make_relative_path_current("a/..".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Input path {input:?} contains relative or absolute components", + input = "a/.." + ) + ); + + s.make_relative_path_current("a/./b".as_ref(), &mut r) + .expect("dot is ignored"); + assert_eq!( + r, + Record { + push_dir: 2, + dirs: vec![".".into(), "./a".into()], + push: 2, + }, + "The `a` directory is pushed, and the leaf, for a total of 2 pushes" + ); + assert_eq!( + s.current().to_string_lossy(), + if cfg!(windows) { ".\\a\\b" } else { "./a/b" }, + "dot is silently ignored" + ); + s.make_relative_path_current("a//b/".as_ref(), &mut r) + .expect("multiple-slashes are ignored"); + assert_eq!( + r, + Record { + push_dir: 2, + dirs: vec![".".into(), "./a".into()], + push: 2, + }, + "nothing changed" + ); +} + +#[test] +fn absolute_paths_are_invalid() -> crate::Result { + let root = PathBuf::from("."); + let mut s = Stack::new(root.clone()); + + let mut r = Record::default(); + let err = s.make_relative_path_current("/".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"/\" contains relative or absolute components", + "a leading slash is always considered absolute" + ); + s.make_relative_path_current("a/".as_ref(), &mut r)?; + assert_eq!( + s.current(), + p("./a/"), + "trailing slashes aren't a problem at this stage, as they cannot cause a 'breakout'" + ); + s.make_relative_path_current("b\\".as_ref(), &mut r)?; + assert_eq!( + s.current(), + p("./b\\"), + "trailing backslashes are fine both on Windows and Unix - on Unix it's part fo the filename" + ); + + #[cfg(windows)] + { + let err = s.make_relative_path_current("\\".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"\\\" contains relative or absolute components", + "on Windows, backslashes are considered absolute and replace the base if it is relative, \ + hence they are forbidden." + ); + + let err = s.make_relative_path_current("c:".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"c:\" contains relative or absolute components", + "on Windows, drive-letters without trailing backslash or slash are also absolute (even though they ought to be relative)" + ); + let err = s.make_relative_path_current("c:\\".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "Input path \"c:\\\" contains relative or absolute components", + "on Windows, drive-letters are absolute, which is expected" + ); + + s.make_relative_path_current("֍:".as_ref(), &mut r)?; + assert_eq!( + s.current().to_string_lossy(), + ".\\֍:", + "on Windows, almost any unicode character will do as virtual drive-letter actually with `subst`, \ + but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path" + ); + let err = s + .make_relative_path_current(r#"\\localhost\hello"#.as_ref(), &mut r) + .unwrap_err(); + assert_eq!( + err.to_string(), + r#"Input path "\\localhost\hello" contains relative or absolute components"#, + "there is UNC paths as well" + ); + + let err = s.make_relative_path_current(r#"\\?\C:"#.as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + r#"Input path "\\?\C:" contains relative or absolute components"#, + "there is UNC paths as well, sometimes they look different" + ); + } + Ok(()) +} + #[test] fn delegate_calls_are_consistent() -> crate::Result { let root = PathBuf::from("."); @@ -43,7 +325,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 2, dirs: dirs.clone(), push: 2, - } + }, + "it pushes the root-directory first, then the intermediate one" ); s.make_relative_path_current("a/b2".as_ref(), &mut r)?; @@ -53,7 +336,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 2, dirs: dirs.clone(), push: 3, - } + }, + "dirs remain the same as b2 is a leaf/file, hence the new `push`" ); s.make_relative_path_current("c/d/e".as_ref(), &mut r)?; @@ -65,7 +349,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 4, dirs: dirs.clone(), push: 6, - } + }, + "each directory is pushed individually, after popping 'a' which isn't included anymore" ); dirs.push(root.join("c").join("d").join("x")); @@ -76,10 +361,11 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 5, dirs: dirs.clone(), push: 8, - } + }, + "a new path component is added, hence `push_dir + 1`, but two components are added in total" ); - dirs.drain(dirs.len() - 3..).count(); + dirs.drain(1..).count(); s.make_relative_path_current("f".as_ref(), &mut r)?; assert_eq!(s.current_relative(), Path::new("f")); assert_eq!( @@ -88,7 +374,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 5, dirs: dirs.clone(), push: 9, - } + }, + "Now we only keep the root, as `f` is a leaf, hence `push + 1`" ); dirs.push(root.join("x")); @@ -99,7 +386,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 6, dirs: dirs.clone(), push: 11, - } + }, + "a new directory is pushed, or two new components total, hence `push + 2`" ); dirs.push(root.join("x").join("z")); @@ -110,7 +398,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 7, dirs: dirs.clone(), push: 12, - } + }, + "and another sub-directory is added" ); dirs.push(root.join("x").join("z").join("a")); @@ -122,10 +411,11 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 9, dirs: dirs.clone(), push: 14, - } + }, + "and more subdirectories, two at once this time." ); - dirs.drain(dirs.len() - 2..).count(); + dirs.drain(1 /*root*/ + 1 /*x*/ + 1 /*x/z*/ ..).count(); s.make_relative_path_current("x/z".as_ref(), &mut r)?; assert_eq!( r, @@ -133,7 +423,8 @@ fn delegate_calls_are_consistent() -> crate::Result { push_dir: 9, dirs: dirs.clone(), push: 14, - } + }, + "this only pops components, and as x/z/a/ was previously a directory, x/z is still a directory" ); assert_eq!( dirs.last(), @@ -141,16 +432,104 @@ fn delegate_calls_are_consistent() -> crate::Result { "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." ); - s.make_relative_path_current("".as_ref(), &mut r)?; + let err = s.make_relative_path_current("".as_ref(), &mut r).unwrap_err(); + assert_eq!( + err.to_string(), + "empty inputs are not allowed", + "this is to protect us from double-counting the root path next time a component is pushed, \ + and besides that really shouldn't happen" + ); + + s.make_relative_path_current("leaf".as_ref(), &mut r)?; + dirs.drain(1..).count(); assert_eq!( r, Record { push_dir: 9, - dirs: vec![".".into()], - push: 14, + dirs: dirs.clone(), + push: 15, }, - "empty-paths reset the tree effectively" + "reset as much as possible, with just a leaf-component and the root directory" ); + s.make_relative_path_current("a//b".as_ref(), &mut r)?; + dirs.push(root.join("a")); + assert_eq!( + r, + Record { + push_dir: 10, + dirs: dirs.clone(), + push: 17, + }, + "double-slashes are automatically cleaned, even though they shouldn't happen, it's not forbidden" + ); + + #[cfg(not(windows))] + { + s.make_relative_path_current("\\/b".as_ref(), &mut r)?; + dirs.pop(); + dirs.push(root.join("\\")); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + "a backslash is a normal character outside of Windows, so it's fine to have it as component" + ); + + s.make_relative_path_current("\\".as_ref(), &mut r)?; + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + "./\\", + "a backslash can also be a valid leaf component - here we only popped the 'b', leaving the \\ 'directory'" + ); + + s.make_relative_path_current("\\\\".as_ref(), &mut r)?; + dirs.pop(); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 20, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + "./\\\\", + "the backslash can also be an ordinary leaf, without the need for it to be a directory" + ); + } + + #[cfg(windows)] + { + s.make_relative_path_current("c\\/d".as_ref(), &mut r)?; + dirs.pop(); + dirs.push(root.join("c")); + assert_eq!( + r, + Record { + push_dir: 11, + dirs: dirs.clone(), + push: 19, + }, + ); + assert_eq!( + s.current().to_string_lossy(), + ".\\c\\d", + "the backslash is a path-separator, and so is the `/`, which is turned into backslash" + ); + } + Ok(()) } diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 9c0dce406e9..201e996b0f1 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -27,6 +27,7 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = [ gix-hash = { version = "^0.14.2", path = "../gix-hash" } gix-bitmap = { version = "^0.2.11", path = "../gix-bitmap" } gix-object = { version = "^0.42.1", path = "../gix-object" } +gix-validate = { version = "^0.8.4", path = "../gix-validate" } gix-traverse = { version = "^0.39.0", path = "../gix-traverse" } gix-lock = { version = "^13.0.0", path = "../gix-lock" } gix-fs = { version = "^0.10.2", path = "../gix-fs" } diff --git a/gix-index/src/entry/mode.rs b/gix-index/src/entry/mode.rs index fca861d2b18..dc3a9a6de94 100644 --- a/gix-index/src/entry/mode.rs +++ b/gix-index/src/entry/mode.rs @@ -67,6 +67,12 @@ impl Mode { } } +impl From for Mode { + fn from(value: gix_object::tree::EntryMode) -> Self { + Self::from_bits_truncate(value.0 as u32) + } +} + /// A change of a [`Mode`]. pub enum Change { /// The type of mode changed, like symlink => file. diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index ecb1f0b13a8..1301df77e9e 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -1,4 +1,6 @@ -mod from_tree { +#[allow(clippy::empty_docs)] +/// +pub mod from_tree { use std::collections::VecDeque; use bstr::{BStr, BString, ByteSlice, ByteVec}; @@ -10,6 +12,19 @@ mod from_tree { Entry, PathStorage, State, Version, }; + /// The error returned by [State::from_tree()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The path \"{path}\" is invalid")] + InvalidComponent { + path: BString, + source: gix_validate::path::component::Error, + }, + #[error(transparent)] + Traversal(#[from] gix_traverse::tree::breadthfirst::Error), + } + /// Initialization impl State { /// Return a new and empty in-memory index assuming the given `object_hash`. @@ -32,23 +47,42 @@ mod from_tree { } /// Create an index [`State`] by traversing `tree` recursively, accessing sub-trees /// with `objects`. + /// `validate` is used to determine which validations to perform on every path component we see. /// /// **No extension data is currently produced**. - pub fn from_tree(tree: &gix_hash::oid, objects: Find) -> Result + pub fn from_tree( + tree: &gix_hash::oid, + objects: Find, + validate: gix_validate::path::component::Options, + ) -> Result where Find: gix_object::Find, { let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); let mut buf = Vec::new(); - let root = objects.find_tree_iter(tree, &mut buf)?; - let mut delegate = CollectEntries::new(); - breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate)?; + let root = objects + .find_tree_iter(tree, &mut buf) + .map_err(breadthfirst::Error::from)?; + let mut delegate = CollectEntries::new(validate); + match breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate) { + Ok(()) => {} + Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => { + let (path, err) = delegate + .invalid_path + .take() + .expect("cancellation only happens on validation error"); + return Err(Error::InvalidComponent { path, source: err }); + } + Err(err) => return Err(err.into()), + } let CollectEntries { mut entries, path_backing, path: _, path_deque: _, + validate: _, + invalid_path: _, } = delegate; entries.sort_by(|a, b| Entry::cmp_filepaths(a.path_in(&path_backing), b.path_in(&path_backing))); @@ -76,15 +110,19 @@ mod from_tree { path_backing: PathStorage, path: BString, path_deque: VecDeque, + validate: gix_validate::path::component::Options, + invalid_path: Option<(BString, gix_validate::path::component::Error)>, } impl CollectEntries { - pub fn new() -> CollectEntries { + pub fn new(validate: gix_validate::path::component::Options) -> CollectEntries { CollectEntries { entries: Vec::new(), path_backing: Vec::new(), path: BString::default(), path_deque: VecDeque::new(), + validate, + invalid_path: None, } } @@ -93,6 +131,11 @@ mod from_tree { self.path.push(b'/'); } self.path.push_str(name); + if self.invalid_path.is_none() { + if let Err(err) = gix_validate::path::component(name, None, self.validate) { + self.invalid_path = Some((self.path.clone(), err)) + } + } } pub fn add_entry(&mut self, entry: &tree::EntryRef<'_>) { @@ -103,6 +146,18 @@ mod from_tree { EntryKind::Link => Mode::SYMLINK, EntryKind::Commit => Mode::COMMIT, }; + // There are leaf-names that require special validation, specific to their mode. + // Double-validate just for this case, as the previous validation didn't know the mode yet. + if self.invalid_path.is_none() { + let start = self.path.rfind_byte(b'/').map(|pos| pos + 1).unwrap_or_default(); + if let Err(err) = gix_validate::path::component( + self.path[start..].as_ref(), + (entry.mode.kind() == EntryKind::Link).then_some(gix_validate::path::component::Mode::Symlink), + self.validate, + ) { + self.invalid_path = Some((self.path.clone(), err)); + } + } let path_start = self.path_backing.len(); self.path_backing.extend_from_slice(&self.path); @@ -117,6 +172,14 @@ mod from_tree { self.entries.push(new_entry); } + + fn determine_action(&self) -> Action { + if self.invalid_path.is_none() { + Action::Continue + } else { + Action::Cancel + } + } } impl Visit for CollectEntries { @@ -127,12 +190,12 @@ mod from_tree { .expect("every call is matched with push_tracked_path_component"); } - fn push_back_tracked_path_component(&mut self, component: &bstr::BStr) { + fn push_back_tracked_path_component(&mut self, component: &BStr) { self.push_element(component); self.path_deque.push_back(self.path.clone()); } - fn push_path_component(&mut self, component: &bstr::BStr) { + fn push_path_component(&mut self, component: &BStr) { self.push_element(component); } @@ -144,13 +207,13 @@ mod from_tree { } } - fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action { - Action::Continue + fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> Action { + self.determine_action() } - fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action { + fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> Action { self.add_entry(entry); - Action::Continue + self.determine_action() } } } diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index a8ff94c0369..1e157bdc0d5 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -26,7 +26,9 @@ pub mod entry; mod access; -mod init; +/// +#[allow(clippy::empty_docs)] +pub mod init; /// #[allow(clippy::empty_docs)] @@ -113,6 +115,21 @@ pub struct AccelerateLookup<'a> { /// /// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree. /// We treat index and its state synonymous. +/// +/// # A note on safety +/// +/// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled +/// by the caller, without applying any level of validation. +/// +/// This means that before using these paths to recreate files on disk, *they must be validated*. +/// +/// It's notable that it's possible to manufacture tree objects which contain names like `.git/hooks/pre-commit` +/// which then will look like `.git/hooks/pre-commit` in the index, which doesn't care that the name came from a single +/// tree instead of from trees named `.git`, `hooks` and a blob named `pre-commit`. The effect is still the same - an invalid +/// path is presented in the index and its consumer must validate each path component before usage. +/// +/// It's recommended to do that using `gix_worktree::Stack` which has it built-in if it's created `for_checkout()`. Alternatively +/// one can validate component names with `gix_validate::path::component()`. #[derive(Clone)] pub struct State { /// The kind of object hash used when storing the underlying file. diff --git a/gix-index/tests/Cargo.toml b/gix-index/tests/Cargo.toml index 4d15e5aed4f..8e5700826f5 100644 --- a/gix-index/tests/Cargo.toml +++ b/gix-index/tests/Cargo.toml @@ -19,8 +19,9 @@ gix-features-parallel = ["gix-features/parallel"] [dev-dependencies] gix-index = { path = ".." } gix-features = { path = "../../gix-features", features = ["rustsha1", "progress"] } -gix-testtools = { path = "../../tests/tools"} -gix = { path = "../../gix", default-features = false, features = ["index"] } -gix-hash = { path = "../../gix-hash"} +gix-testtools = { path = "../../tests/tools" } +gix-odb = { path = "../../gix-odb" } +gix-object = { path = "../../gix-object" } +gix-hash = { path = "../../gix-hash" } filetime = "0.2.15" bstr = { version = "1.3.0", default-features = false } diff --git a/gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz b/gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz new file mode 100644 index 00000000000..3e93c175191 Binary files /dev/null and b/gix-index/tests/fixtures/generated-archives/make_traverse_literal_separators.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v2.tar.xz b/gix-index/tests/fixtures/generated-archives/v2.tar.xz index 42e49dc2c72..596f9af672c 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v2.tar.xz and b/gix-index/tests/fixtures/generated-archives/v2.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz b/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz index 169e964ba09..63b0c86054d 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz and b/gix-index/tests/fixtures/generated-archives/v2_all_file_kinds.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v2_more_files.tar.xz b/gix-index/tests/fixtures/generated-archives/v2_more_files.tar.xz index 4f257c10db9..71a4516a603 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v2_more_files.tar.xz and b/gix-index/tests/fixtures/generated-archives/v2_more_files.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v2_sparse_index_no_dirs.tar.xz b/gix-index/tests/fixtures/generated-archives/v2_sparse_index_no_dirs.tar.xz index dac194d03cf..126c3dd4d74 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v2_sparse_index_no_dirs.tar.xz and b/gix-index/tests/fixtures/generated-archives/v2_sparse_index_no_dirs.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v3_sparse_index_non_cone.tar.xz b/gix-index/tests/fixtures/generated-archives/v3_sparse_index_non_cone.tar.xz index ee53ba29c48..d4f4f8094ca 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v3_sparse_index_non_cone.tar.xz and b/gix-index/tests/fixtures/generated-archives/v3_sparse_index_non_cone.tar.xz differ diff --git a/gix-index/tests/fixtures/generated-archives/v4_more_files_IEOT.tar.xz b/gix-index/tests/fixtures/generated-archives/v4_more_files_IEOT.tar.xz index eb191c2d5fd..e1d153f5f94 100644 Binary files a/gix-index/tests/fixtures/generated-archives/v4_more_files_IEOT.tar.xz and b/gix-index/tests/fixtures/generated-archives/v4_more_files_IEOT.tar.xz differ diff --git a/gix-index/tests/fixtures/make_index/v2.sh b/gix-index/tests/fixtures/make_index/v2.sh index a61c9527d01..3b95f64b476 100755 --- a/gix-index/tests/fixtures/make_index/v2.sh +++ b/gix-index/tests/fixtures/make_index/v2.sh @@ -8,3 +8,5 @@ git config index.threads 2 touch a git add a git commit -m "empty" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh b/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh index b7a4610da2f..5869a181928 100755 --- a/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh +++ b/gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh @@ -22,3 +22,5 @@ mkdir d git add . git commit -m "init" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_more_files.sh b/gix-index/tests/fixtures/make_index/v2_more_files.sh index d4cafddc097..11d6fb12b15 100755 --- a/gix-index/tests/fixtures/make_index/v2_more_files.sh +++ b/gix-index/tests/fixtures/make_index/v2_more_files.sh @@ -11,3 +11,5 @@ mkdir d git add . git commit -m "empty" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh b/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh index 890483b4276..9c6275a9927 100755 --- a/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh +++ b/gix-index/tests/fixtures/make_index/v2_sparse_index_no_dirs.sh @@ -17,4 +17,4 @@ git config --worktree index.sparse true echo "/*" > .git/info/sparse-checkout && echo "!/*/" >> .git/info/sparse-checkout -git checkout main \ No newline at end of file +git checkout main diff --git a/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh b/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh index 542d9c8ba2d..36e8f4ff6d7 100755 --- a/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh +++ b/gix-index/tests/fixtures/make_index/v3_sparse_index_non_cone.sh @@ -13,4 +13,4 @@ mkdir d git add . git commit -m "init" -git sparse-checkout set c1/c2 --no-cone \ No newline at end of file +git sparse-checkout set c1/c2 --no-cone diff --git a/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh b/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh index 3b22aaa6692..fe6368e096b 100755 --- a/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh +++ b/gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh @@ -12,3 +12,5 @@ touch x git add . git commit -m "empty" + +git rev-parse @^{tree} > head.tree diff --git a/gix-index/tests/fixtures/make_traverse_literal_separators.sh b/gix-index/tests/fixtures/make_traverse_literal_separators.sh new file mode 100644 index 00000000000..a29fe25fa22 --- /dev/null +++ b/gix-index/tests/fixtures/make_traverse_literal_separators.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -eu -o pipefail + +# Makes a repo carrying a literally named file, which may even contain "/". +# File content is from stdin. Arguments are repo name, file name, and file mode. +function make_repo() ( + local repo="$1" file="$2" mode="$3" + local blob_hash_escaped tree_hash commit_hash branch + + git init -- "$repo" + cd -- "$repo" # Temporary, as the function body is a ( ) subshell. + + blob_hash_escaped="$(git hash-object -w --stdin | sed 's/../\\x&/g')" + + tree_hash="$( + printf "%s %s\\0$blob_hash_escaped" "$mode" "$file" | + git hash-object -t tree -w --stdin --literally + )" + + commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")" + + branch="$(git symbolic-ref --short HEAD)" + git branch -f -- "$branch" "$commit_hash" + test -z "${DEBUG_FIXTURE-}" || git show # TODO: How should verbosity be controlled? + git rev-parse @^{tree} > head.tree +) + +make_repo traverse_dotdot_slashes ../outside 100644 \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF + +make_repo traverse_dotdot_backslashes '..\outside' 100644 \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_backslashes '.git\hooks\pre-commit' 100755 <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF \ No newline at end of file diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 53387266f4c..bf1804384be 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -11,7 +11,7 @@ use crate::{hex_to_id, index::Fixture, loose_file_path}; fn verify(index: gix_index::File) -> gix_index::File { index.verify_integrity().unwrap(); index.verify_entries().unwrap(); - index.verify_extensions(false, gix::objs::find::Never).unwrap(); + index.verify_extensions(false, gix_object::find::Never).unwrap(); index } diff --git a/gix-index/tests/index/file/write.rs b/gix-index/tests/index/file/write.rs index a01963c8e31..10d924b4e21 100644 --- a/gix-index/tests/index/file/write.rs +++ b/gix-index/tests/index/file/write.rs @@ -228,7 +228,7 @@ fn compare_states_against_baseline( fn compare_states(actual: &State, actual_version: Version, expected: &State, options: Options, fixture: &str) { actual.verify_entries().expect("valid"); - actual.verify_extensions(false, gix::objs::find::Never).expect("valid"); + actual.verify_extensions(false, gix_object::find::Never).expect("valid"); assert_eq!( actual.version(), diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 22595526605..cc3b0494119 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -1,5 +1,7 @@ use gix_index::State; use gix_testtools::scripted_fixture_read_only_standalone; +use std::error::Error; +use std::path::Path; #[test] fn from_tree() -> crate::Result { @@ -11,19 +13,45 @@ fn from_tree() -> crate::Result { ]; for fixture in fixtures { - let repo_dir = scripted_fixture_read_only_standalone(fixture)?; - let repo = gix::open(&repo_dir)?; + let worktree_dir = scripted_fixture_read_only_standalone(fixture)?; - let tree_id = repo.head_commit()?.tree_id()?; + let tree_id = tree_id(&worktree_dir); - let expected_state = repo.index()?; - let actual_state = State::from_tree(&tree_id, &repo.objects)?; + let git_dir = worktree_dir.join(".git"); + let expected_state = + gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; + let odb = gix_odb::at(git_dir.join("objects"))?; + let actual_state = State::from_tree(&tree_id, &odb, Default::default())?; compare_states(&actual_state, &expected_state, fixture) } Ok(()) } +#[test] +fn from_tree_validation() -> crate::Result { + let root = scripted_fixture_read_only_standalone("make_traverse_literal_separators.sh")?; + for repo_name in [ + "traverse_dotdot_slashes", + "traverse_dotgit_slashes", + "traverse_dotgit_backslashes", + "traverse_dotdot_backslashes", + ] { + let worktree_dir = root.join(repo_name); + let tree_id = tree_id(&worktree_dir); + let git_dir = worktree_dir.join(".git"); + let odb = gix_odb::at(git_dir.join("objects"))?; + + let err = State::from_tree(&tree_id, &odb, Default::default()).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "Path separators like / or \\ are not allowed", + "Note that this effectively tests what would happen on Windows, where \\ also isn't allowed" + ); + } + Ok(()) +} + #[test] fn new() { let state = State::new(gix_hash::Kind::Sha1); @@ -34,7 +62,7 @@ fn new() { fn compare_states(actual: &State, expected: &State, fixture: &str) { actual.verify_entries().expect("valid"); - actual.verify_extensions(false, gix::objs::find::Never).expect("valid"); + actual.verify_extensions(false, gix_object::find::Never).expect("valid"); assert_eq!( actual.entries().len(), @@ -49,3 +77,9 @@ fn compare_states(actual: &State, expected: &State, fixture: &str) { assert_eq!(a.path(actual), e.path(expected), "entry path mismatch in {fixture:?}"); } } + +fn tree_id(root: &Path) -> gix_hash::ObjectId { + let hex_hash = + std::fs::read_to_string(root.join("head.tree")).expect("head.tree was created by git rev-parse @^{tree}"); + hex_hash.trim().parse().expect("valid hash") +} diff --git a/gix-negotiate/tests/baseline/mod.rs b/gix-negotiate/tests/baseline/mod.rs index 158d70a4be3..f416097f428 100644 --- a/gix-negotiate/tests/baseline/mod.rs +++ b/gix-negotiate/tests/baseline/mod.rs @@ -26,9 +26,10 @@ fn run() -> crate::Result { let store = gix_odb::at(base.join("client").join(".git/objects"))?; let refs = gix_ref::file::Store::at( base.join("client").join(".git"), - WriteReflog::Disable, - gix_hash::Kind::Sha1, - false, + gix_ref::store::init::Options { + write_reflog: WriteReflog::Disable, + ..Default::default() + }, ); let lookup_names = |names: &[&str]| -> Vec { names diff --git a/gix-pathspec/Cargo.toml b/gix-pathspec/Cargo.toml index 08f8d700d57..5fe702c59cd 100644 --- a/gix-pathspec/Cargo.toml +++ b/gix-pathspec/Cargo.toml @@ -18,11 +18,11 @@ gix-path = { version = "^0.10.7", path = "../gix-path" } gix-attributes = { version = "^0.22.2", path = "../gix-attributes" } gix-config-value = { version = "^0.14.6", path = "../gix-config-value" } -bstr = { version = "1.3.0", default-features = false, features = ["std"]} +bstr = { version = "1.3.0", default-features = false, features = ["std"] } bitflags = "2" thiserror = "1.0.26" [dev-dependencies] gix-testtools = { path = "../tests/tools" } once_cell = "1.12.0" -serial_test = "2.0.0" +serial_test = "3.1.1" diff --git a/gix-prompt/Cargo.toml b/gix-prompt/Cargo.toml index 66f76893dd2..a6ac0893d06 100644 --- a/gix-prompt/Cargo.toml +++ b/gix-prompt/Cargo.toml @@ -23,6 +23,6 @@ parking_lot = "0.12.1" rustix = { version = "0.38.4", features = ["termios"] } [dev-dependencies] -gix-testtools = { path = "../tests/tools"} -serial_test = { version = "2.0.0", default-features = false } +gix-testtools = { path = "../tests/tools" } +serial_test = { version = "3.1.0", default-features = false } expectrl = "0.7.0" diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index ecf54ed49fe..29028b38d62 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -62,6 +62,25 @@ pub mod peel; /// #[allow(clippy::empty_docs)] pub mod store { + /// + #[allow(clippy::empty_docs)] + pub mod init { + + /// Options for use during [initialization](crate::file::Store::at). + #[derive(Debug, Copy, Clone, Default)] + pub struct Options { + /// How to write the ref-log. + pub write_reflog: super::WriteReflog, + /// The kind of hash to expect in + pub object_hash: gix_hash::Kind, + /// The equivalent of `core.precomposeUnicode`. + pub precompose_unicode: bool, + /// If `true`, we will avoid reading from or writing to references that contains Windows device names + /// to avoid side effects. This only needs to be `true` on Windows, but can be `true` on other platforms + /// if they need to remain compatible with Windows. + pub prohibit_windows_device_names: bool, + } + } /// The way a file store handles the reflog #[derive(Default, Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone, Copy)] pub enum WriteReflog { @@ -93,9 +112,8 @@ pub mod store { /// #[path = "general/handle/mod.rs"] mod handle; - pub use handle::find; - use crate::file; + pub use handle::find; } /// The git reference store. diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index b8a45e86b2e..b148d3f2e73 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -251,8 +251,20 @@ impl file::Store { /// Read the file contents with a verified full reference path and return it in the given vector if possible. pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result>> { - let ref_path = self.reference_path(name); + let (base, relative_path) = self.reference_path_with_base(name); + if self.prohibit_windows_device_names + && relative_path + .components() + .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok()) + .any(|c| gix_validate::path::component_is_windows_device(c.as_ref())) + { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Illegal use of reserved Windows device name in \"{}\"", name.as_bstr()), + )); + } + let ref_path = base.join(relative_path); match std::fs::File::open(&ref_path) { Ok(mut file) => { let mut buf = Vec::with_capacity(128); diff --git a/gix-ref/src/store/file/loose/mod.rs b/gix-ref/src/store/file/loose/mod.rs index f1fd8735163..57c1317d8dd 100644 --- a/gix-ref/src/store/file/loose/mod.rs +++ b/gix-ref/src/store/file/loose/mod.rs @@ -35,15 +35,18 @@ mod init { impl file::Store { /// Create a new instance at the given `git_dir`, which commonly is a standard git repository with a /// `refs/` subdirectory. - /// The `object_hash` defines which kind of hash we should recognize. + /// Use [`Options`](crate::store::init::Options) to adjust settings. /// - /// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, + /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. pub fn at( git_dir: PathBuf, - write_reflog: file::WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, + crate::store::init::Options { + write_reflog, + object_hash, + precompose_unicode, + prohibit_windows_device_names, + }: crate::store::init::Options, ) -> Self { file::Store { git_dir, @@ -51,6 +54,7 @@ mod init { common_dir: None, write_reflog, namespace: None, + prohibit_windows_device_names, packed: gix_fs::SharedFileSnapshotMut::new().into(), object_hash, precompose_unicode, @@ -60,14 +64,17 @@ mod init { /// Like [`at()`][file::Store::at()], but for _linked_ work-trees which use `git_dir` as private ref store and `common_dir` for /// shared references. /// - /// Note that if `precompose_unicode` is set, the `git_dir` and `common_dir` are also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set, the `git_dir` and + /// `common_dir` are also expected to use precomposed unicode, or else some operations that strip prefixes will fail. pub fn for_linked_worktree( git_dir: PathBuf, common_dir: PathBuf, - write_reflog: file::WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, + crate::store::init::Options { + write_reflog, + object_hash, + precompose_unicode, + prohibit_windows_device_names, + }: crate::store::init::Options, ) -> Self { file::Store { git_dir, @@ -75,6 +82,7 @@ mod init { common_dir: Some(common_dir), write_reflog, namespace: None, + prohibit_windows_device_names, packed: gix_fs::SharedFileSnapshotMut::new().into(), object_hash, precompose_unicode, diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index c2b487ad9e2..45d74cead09 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -14,7 +14,13 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { fn empty_store(writemode: WriteReflog) -> Result<(TempDir, file::Store)> { let dir = TempDir::new()?; - let store = file::Store::at(dir.path().into(), writemode, gix_hash::Kind::Sha1, false); + let store = file::Store::at( + dir.path().into(), + crate::store::init::Options { + write_reflog: writemode, + ..Default::default() + }, + ); Ok((dir, store)) } diff --git a/gix-ref/src/store/file/mod.rs b/gix-ref/src/store/file/mod.rs index 19296b0af3f..c01c9deb4d8 100644 --- a/gix-ref/src/store/file/mod.rs +++ b/gix-ref/src/store/file/mod.rs @@ -27,6 +27,9 @@ pub struct Store { pub write_reflog: WriteReflog, /// The namespace to use for edits and reads pub namespace: Option, + /// This is only needed on Windows, where some device names are reserved at any level of a path, so that + /// reading or writing `refs/heads/CON` for example would read from the console, or write to it. + pub prohibit_windows_device_names: bool, /// If set, we will convert decomposed unicode like `a\u308` into precomposed unicode like `ä` when reading /// ref names from disk. /// Note that this is an internal operation that isn't observable on the outside, but it's needed for lookups diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index afb1dd21489..79d86c6cbd5 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -51,7 +51,7 @@ impl<'s, 'p> Transaction<'s, 'p> { .map_err(Error::from), (None, None) => Ok(None), (maybe_loose, _) => Ok(maybe_loose), - }); + })?; let lock = match &mut change.update.change { Change::Delete { expected, .. } => { let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref()); @@ -70,7 +70,6 @@ impl<'s, 'p> Transaction<'s, 'p> { .into() }; - let existing_ref = existing_ref?; match (&expected, &existing_ref) { (PreviousValue::MustNotExist, _) => { panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted") @@ -120,7 +119,6 @@ impl<'s, 'p> Transaction<'s, 'p> { }; let mut lock = (!has_global_lock).then(obtain_lock).transpose()?; - let existing_ref = existing_ref?; match (&expected, &existing_ref) { (PreviousValue::Any, _) | (PreviousValue::MustExist, Some(_)) diff --git a/gix-ref/src/store/general/init.rs b/gix-ref/src/store/general/init.rs index 6b5ee9e87f2..efe5dacfaf5 100644 --- a/gix-ref/src/store/general/init.rs +++ b/gix-ref/src/store/general/init.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; -use crate::store::WriteReflog; - mod error { /// The error returned by [`crate::Store::at()`]. #[derive(Debug, thiserror::Error)] @@ -19,23 +17,16 @@ use crate::file; #[allow(dead_code)] impl crate::Store { /// Create a new store at the given location, typically the `.git/` directory. + /// Use [`opts`](crate::store::init::Options) to adjust settings. /// - /// `object_hash` defines the kind of hash to assume when dealing with refs. - /// `precompose_unicode` is used to set to the value of [`crate::file::Store::precompose_unicode]. - /// - /// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode, - /// or else some operations that strip prefixes will fail. - pub fn at( - git_dir: PathBuf, - reflog_mode: WriteReflog, - object_hash: gix_hash::Kind, - precompose_unicode: bool, - ) -> Result { + /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, + /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. + pub fn at(git_dir: PathBuf, opts: crate::store::init::Options) -> Result { // for now, just try to read the directory - later we will do that naturally as we have to figure out if it's a ref-table or not. std::fs::read_dir(&git_dir)?; Ok(crate::Store { inner: crate::store::State::Loose { - store: file::Store::at(git_dir, reflog_mode, object_hash, precompose_unicode), + store: file::Store::at(git_dir, opts), }, }) } diff --git a/gix-ref/tests/file/mod.rs b/gix-ref/tests/file/mod.rs index 3f932075af8..9c2aa843418 100644 --- a/gix-ref/tests/file/mod.rs +++ b/gix-ref/tests/file/mod.rs @@ -15,26 +15,13 @@ pub fn store_with_packed_refs() -> crate::Result { pub fn store_at(name: &str) -> crate::Result { let path = gix_testtools::scripted_fixture_read_only_standalone(name)?; - Ok(Store::at( - path.join(".git"), - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - )) + Ok(Store::at(path.join(".git"), Default::default())) } fn store_writable(name: &str) -> crate::Result<(gix_testtools::tempfile::TempDir, Store)> { let dir = gix_testtools::scripted_fixture_writable_standalone(name)?; let git_dir = dir.path().join(".git"); - Ok(( - dir, - Store::at( - git_dir, - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - ), - )) + Ok((dir, Store::at(git_dir, Default::default()))) } struct EmptyCommit; diff --git a/gix-ref/tests/file/store/mod.rs b/gix-ref/tests/file/store/mod.rs index a3e7db39164..4cfd6120b03 100644 --- a/gix-ref/tests/file/store/mod.rs +++ b/gix-ref/tests/file/store/mod.rs @@ -21,9 +21,11 @@ fn precompose_unicode_journey() -> crate::Result { let store_decomposed = gix_ref::file::Store::at( root, - WriteReflog::Always, - gix_hash::Kind::Sha1, - false, /* precompose_unicode */ + gix_ref::store::init::Options { + write_reflog: WriteReflog::Always, + precompose_unicode: false, + ..Default::default() + }, ); assert!(!store_decomposed.precompose_unicode); @@ -46,9 +48,11 @@ fn precompose_unicode_journey() -> crate::Result { let store_precomposed = gix_ref::file::Store::at( tmp.path().join(precomposed_a), // it's important that root paths are also precomposed then. - WriteReflog::Always, - gix_hash::Kind::Sha1, - true, /* precompose_unicode */ + gix_ref::store::init::Options { + write_reflog: WriteReflog::Always, + precompose_unicode: true, + ..Default::default() + }, ); let precomposed_ref = format!("refs/heads/{precomposed_a}"); diff --git a/gix-ref/tests/file/store/reflog.rs b/gix-ref/tests/file/store/reflog.rs index 27ffdd82630..a0a2ad3b124 100644 --- a/gix-ref/tests/file/store/reflog.rs +++ b/gix-ref/tests/file/store/reflog.rs @@ -1,9 +1,10 @@ fn store() -> crate::Result { Ok(crate::file::Store::at( gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_reflog.sh")?.join(".git"), - gix_ref::store::WriteReflog::Disable, - gix_hash::Kind::Sha1, - false, + gix_ref::store::init::Options { + write_reflog: gix_ref::store::WriteReflog::Disable, + ..Default::default() + }, )) } diff --git a/gix-ref/tests/file/transaction/mod.rs b/gix-ref/tests/file/transaction/mod.rs index e7f8d344e38..348f76df6e7 100644 --- a/gix-ref/tests/file/transaction/mod.rs +++ b/gix-ref/tests/file/transaction/mod.rs @@ -22,12 +22,7 @@ pub(crate) mod prepare_and_commit { pub(crate) fn empty_store() -> crate::Result<(gix_testtools::tempfile::TempDir, file::Store)> { let dir = gix_testtools::tempfile::TempDir::new().unwrap(); - let store = file::Store::at( - dir.path().into(), - gix_ref::store::WriteReflog::Normal, - gix_hash::Kind::Sha1, - false, - ); + let store = file::Store::at(dir.path().into(), Default::default()); Ok((dir, store)) } diff --git a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs index 99ec59d673d..6c71b98ca76 100644 --- a/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -10,6 +10,7 @@ use gix_ref::{ transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, Target, }; +use std::error::Error; use crate::{ file::{ @@ -430,6 +431,63 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> crate::Result Ok(()) } +#[test] +fn windows_device_name_is_illegal_with_enabled_windows_protections() -> crate::Result { + let (_keep, mut store) = empty_store()?; + store.prohibit_windows_device_names = true; + let log_ignored = LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "ignored".into(), + }; + + let new = Target::Peeled(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242")); + for invalid_name in ["refs/heads/CON", "refs/CON/still-invalid"] { + let err = store + .transaction() + .prepare( + Some(RefEdit { + change: Change::Update { + log: log_ignored.clone(), + new: new.clone(), + expected: PreviousValue::Any, + }, + name: invalid_name.try_into()?, + deref: false, + }), + Fail::Immediately, + Fail::Immediately, + ) + .unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + format!("Illegal use of reserved Windows device name in \"{invalid_name}\""), + "it's notable that the check also kicks in when the previous value doesn't matter - we expect a 'read' to happen anyway \ + - it can't be optimized away as the previous value is stored in the transaction result right now." + ); + } + + #[cfg(not(windows))] + { + store.prohibit_windows_device_names = false; + let _prepared_transaction = store.transaction().prepare( + Some(RefEdit { + change: Change::Update { + log: log_ignored.clone(), + new, + expected: PreviousValue::Any, + }, + name: "refs/heads/CON".try_into()?, + deref: false, + }), + Fail::Immediately, + Fail::Immediately, + )?; + } + + Ok(()) +} + #[test] fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { for reflog_writemode in &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always] { diff --git a/gix-ref/tests/file/worktree.rs b/gix-ref/tests/file/worktree.rs index 196ac3d1260..7b723eca107 100644 --- a/gix-ref/tests/file/worktree.rs +++ b/gix-ref/tests/file/worktree.rs @@ -29,7 +29,7 @@ fn main_store( let (dir, tmp) = dir(packed, writable)?; let git_dir = dir.join("repo").join(".git"); Ok(( - gix_ref::file::Store::at(git_dir.clone(), Default::default(), Default::default(), false), + gix_ref::file::Store::at(git_dir.clone(), Default::default()), gix_odb::at(git_dir.join("objects"))?, tmp, )) @@ -50,13 +50,7 @@ fn worktree_store( .into_repository_and_work_tree_directories(); let common_dir = git_dir.join("../.."); Ok(( - gix_ref::file::Store::for_linked_worktree( - git_dir, - common_dir.clone(), - Default::default(), - Default::default(), - false, - ), + gix_ref::file::Store::for_linked_worktree(git_dir, common_dir.clone(), Default::default()), gix_odb::at(common_dir.join("objects"))?, tmp, )) diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index dbe7a838ed2..cf2e8ea9e51 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -19,7 +19,7 @@ use crate::{ types::{Error, Options}, Change, Conflict, EntryStatus, Outcome, VisitEntry, }, - SymlinkCheck, + is_dir_to_mode, SymlinkCheck, }; /// Calculates the changes that need to be applied to an `index` to match the state of the `worktree` and makes them @@ -276,7 +276,7 @@ impl<'index> State<'_, 'index> { &mut |relative_path, case, is_dir, out| { self.attr_stack .set_case(case) - .at_entry(relative_path, Some(is_dir), objects) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), objects) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -541,7 +541,9 @@ where } } else { self.buf.clear(); - let platform = self.attr_stack.at_entry(self.rela_path, Some(false), &self.objects)?; + let platform = self + .attr_stack + .at_entry(self.rela_path, Some(self.entry.mode), &self.objects)?; let file = std::fs::File::open(self.path)?; let out = self .filter diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 0932e7d4f4d..a3953bbed78 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -9,6 +9,7 @@ pub(super) mod function { use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; use crate::index_as_worktree_with_renames::function::rewrite::ModificationOrDirwalkEntry; use crate::index_as_worktree_with_renames::{Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry}; + use crate::is_dir_to_mode; use bstr::ByteSlice; use gix_worktree::stack::State; use std::borrow::Cow; @@ -99,7 +100,7 @@ pub(super) mod function { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &objects) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &objects) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: excludes.as_mut(), @@ -494,7 +495,7 @@ pub(super) mod function { Ok(match kind { Kind::File => { let platform = attrs - .at_entry(rela_path, Some(false), objects) + .at_entry(rela_path, None, objects) .map_err(Error::SetAttributeContext)?; let rela_path = gix_path::from_bstr(rela_path); let file_path = worktree_root.join(rela_path.as_ref()); diff --git a/gix-status/src/lib.rs b/gix-status/src/lib.rs index a2dbf6a4c51..86532fbad16 100644 --- a/gix-status/src/lib.rs +++ b/gix-status/src/lib.rs @@ -32,3 +32,11 @@ pub struct SymlinkCheck { } mod stack; + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index 2e907e10f40..abecff15d49 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -102,7 +102,7 @@ futures-lite = { workspace = true, optional = true } pin-project-lite = { version = "0.2.6", optional = true } # for http-client -base64 = { version = "0.21.0", optional = true } +base64 = { version = "0.22.1", optional = true } # for http-client-curl. Additional configuration should be performed on higher levels of the dependency tree. curl = { workspace = true, optional = true } diff --git a/gix-validate/src/lib.rs b/gix-validate/src/lib.rs index f0493960c73..0143187a851 100644 --- a/gix-validate/src/lib.rs +++ b/gix-validate/src/lib.rs @@ -13,3 +13,7 @@ pub mod tag; /// #[allow(clippy::empty_docs)] pub mod submodule; + +/// +#[allow(clippy::empty_docs)] +pub mod path; diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs new file mode 100644 index 00000000000..6a18fe7468e --- /dev/null +++ b/gix-validate/src/path.rs @@ -0,0 +1,336 @@ +use bstr::{BStr, ByteSlice}; + +/// +#[allow(clippy::empty_docs)] +pub mod component { + /// The error returned by [`component()`](super::component()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("A path component must not be empty")] + Empty, + #[error("Path separators like / or \\ are not allowed")] + PathSeparator, + #[error("Windows path prefixes are not allowed")] + WindowsPathPrefix, + #[error("Windows device-names may have side-effects and are not allowed")] + WindowsReservedName, + #[error("Trailing spaces or dots, and the following characters anywhere, are forbidden in Windows paths, along with non-printable ones: <>:\"|?*")] + WindowsIllegalCharacter, + #[error("The .git name may never be used")] + DotGitDir, + #[error("The .gitmodules file must not be a symlink")] + SymlinkedGitModules, + } + + /// Further specify what to check for in [`component()`](super::component()) + /// + /// Note that the `Default` implementation maximizes safety by enabling all protections. + #[derive(Debug, Copy, Clone)] + pub struct Options { + /// This flag should be turned on when on Windows, but can be turned on when on other platforms + /// as well to prevent path components that can cause trouble on Windows. + pub protect_windows: bool, + /// If `true`, protections for the MacOS HFS+ filesystem will be active, checking for + /// special directories that we should never write while ignoring codepoints just like HFS+ would. + /// + /// This field is equivalent to `core.protectHFS`. + pub protect_hfs: bool, + /// If `true`, protections for Windows NTFS specific features will be active. This adds special handling + /// for `8.3` filenames and alternate data streams, both of which could be used to mask the true name of + /// what would be created on disk. + /// + /// This field is equivalent to `core.protectNTFS`. + pub protect_ntfs: bool, + } + + impl Default for Options { + fn default() -> Self { + Options { + protect_windows: true, + protect_hfs: true, + protect_ntfs: true, + } + } + } + + /// The mode of the component, if it's the leaf of a path. + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum Mode { + /// The item is a symbolic link. + Symlink, + } +} + +/// Assure the given `input` resembles a valid name for a tree or blob, and in that sense, a path component. +/// `mode` indicates the kind of `input` and it should be `Some` if `input` is the last component in the underlying +/// path. +/// +/// `input` must not make it possible to exit the repository, or to specify absolute paths. +pub fn component( + input: &BStr, + mode: Option, + component::Options { + protect_windows, + protect_hfs, + protect_ntfs, + }: component::Options, +) -> Result<&BStr, component::Error> { + if input.is_empty() { + return Err(component::Error::Empty); + } + if protect_windows { + if input.find_byteset(b"/\\").is_some() { + return Err(component::Error::PathSeparator); + } + if input.chars().nth(1) == Some(':') { + return Err(component::Error::WindowsPathPrefix); + } + } else if input.find_byte(b'/').is_some() { + return Err(component::Error::PathSeparator); + } + if protect_hfs { + if is_dot_hfs(input, "git") { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && is_dot_hfs(input, "gitmodules") { + return Err(component::Error::SymlinkedGitModules); + } + } + + if protect_ntfs { + if is_dot_git_ntfs(input) { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && is_dot_ntfs(input, "gitmodules", "gi7eba") { + return Err(component::Error::SymlinkedGitModules); + } + + if protect_windows { + if let Some(err) = check_win_devices_and_illegal_characters(input) { + return Err(err); + } + } + } + + if !(protect_hfs | protect_ntfs) { + if input.eq_ignore_ascii_case(b".git") { + return Err(component::Error::DotGitDir); + } + if is_symlink(mode) && input.eq_ignore_ascii_case(b".gitmodules") { + return Err(component::Error::SymlinkedGitModules); + } + } + Ok(input) +} + +/// Return `true` if the path component at `input` looks like a Windows device, like `CON` +/// or `LPT1` (case-insensitively). +/// +/// This is relevant only on Windows, where one may be tricked into reading or writing to such devices. +/// When reading from `CON`, a console-program may block until the user provided input. +pub fn component_is_windows_device(input: &BStr) -> bool { + is_win_device(input) +} + +fn is_win_device(input: &BStr) -> bool { + let Some(in3) = input.get(..3) else { return false }; + if in3.eq_ignore_ascii_case(b"AUX") && is_done_windows(input.get(3..)) { + return true; + } + if in3.eq_ignore_ascii_case(b"NUL") && is_done_windows(input.get(3..)) { + return true; + } + if in3.eq_ignore_ascii_case(b"PRN") && is_done_windows(input.get(3..)) { + return true; + } + // Note that the following allows `COM0`, even though `LPT0` is not allowed. + // Even though tests seem to indicate that neither `LPT0` nor `COM0` are valid + // device names, it's unclear this truly is the case in all possible versions and editions + // of Windows. + // Hence, justification for this asymmetry is merely to do exactly the same as Git does, + // and to have exactly the same behaviour during validation (for worktree-writes). + if in3.eq_ignore_ascii_case(b"COM") + && input.get(3).map_or(false, |n| *n >= b'1' && *n <= b'9') + && is_done_windows(input.get(4..)) + { + return true; + } + if in3.eq_ignore_ascii_case(b"LPT") + && input.get(3).map_or(false, u8::is_ascii_digit) + && is_done_windows(input.get(4..)) + { + return true; + } + if in3.eq_ignore_ascii_case(b"CON") + && (is_done_windows(input.get(3..)) + || (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"IN$")) && is_done_windows(input.get(6..))) + || (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"OUT$")) && is_done_windows(input.get(7..)))) + { + return true; + } + false +} + +fn check_win_devices_and_illegal_characters(input: &BStr) -> Option { + if is_win_device(input) { + return Some(component::Error::WindowsReservedName); + } + if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) { + return Some(component::Error::WindowsIllegalCharacter); + } + if input.ends_with(b".") || input.ends_with(b" ") { + return Some(component::Error::WindowsIllegalCharacter); + } + None +} + +fn is_symlink(mode: Option) -> bool { + mode.map_or(false, |m| m == component::Mode::Symlink) +} + +fn is_dot_hfs(input: &BStr, search_case_insensitive: &str) -> bool { + let mut input = input.chars().filter(|c| match *c as u32 { + // Case-insensitive HFS+ skips these code points as "ignorable" when comparing filenames. See: + // https://github.com/git/git/commit/6162a1d323d24fd8cbbb1a6145a91fb849b2568f + // https://developer.apple.com/library/archive/technotes/tn/tn1150.html#StringComparisonAlgorithm + // https://github.com/apple-oss-distributions/hfs/blob/main/core/UCStringCompareData.h + 0x200c | // ZERO WIDTH NON-JOINER + 0x200d | // ZERO WIDTH JOINER + 0x200e | // LEFT-TO-RIGHT MARK + 0x200f | // RIGHT-TO-LEFT MARK + 0x202a | // LEFT-TO-RIGHT EMBEDDING + 0x202b | // RIGHT-TO-LEFT EMBEDDING + 0x202c | // POP DIRECTIONAL FORMATTING + 0x202d | // LEFT-TO-RIGHT OVERRIDE + 0x202e | // RIGHT-TO-LEFT OVERRIDE + 0x206a | // INHIBIT SYMMETRIC SWAPPING + 0x206b | // ACTIVATE SYMMETRIC SWAPPING + 0x206c | // INHIBIT ARABIC FORM SHAPING + 0x206d | // ACTIVATE ARABIC FORM SHAPING + 0x206e | // NATIONAL DIGIT SHAPES + 0x206f | // NOMINAL DIGIT SHAPES + 0xfeff => false, // ZERO WIDTH NO-BREAK SPACE + _ => true + }); + if input.next() != Some('.') { + return false; + } + + let mut comp = search_case_insensitive.chars(); + loop { + match (comp.next(), input.next()) { + (Some(a), Some(b)) => { + if !a.eq_ignore_ascii_case(&b) { + return false; + } + } + (None, None) => return true, + _ => return false, + } + } +} + +fn is_dot_git_ntfs(input: &BStr) -> bool { + if input + .get(..4) + .map_or(false, |input| input.eq_ignore_ascii_case(b".git")) + { + return is_done_ntfs(input.get(4..)); + } + if input + .get(..5) + .map_or(false, |input| input.eq_ignore_ascii_case(b"git~1")) + { + return is_done_ntfs(input.get(5..)); + } + false +} + +/// The `search_case_insensitive` name is the actual name to look for (in a case-insensitive way). +/// Opposed to that there is the special `ntfs_shortname_prefix` which is derived from `search_case_insensitive` +/// but looks more like a hash, one that NTFS uses to disambiguate things, for when there is a lot of files +/// with the same prefix. +fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefix: &str) -> bool { + if input.first() == Some(&b'.') { + let end_pos = 1 + search_case_insensitive.len(); + if input.get(1..end_pos).map_or(false, |input| { + input.eq_ignore_ascii_case(search_case_insensitive.as_bytes()) + }) { + is_done_ntfs(input.get(end_pos..)) + } else { + false + } + } else { + let search_case_insensitive: &[u8] = search_case_insensitive.as_bytes(); + if search_case_insensitive + .get(..6) + .zip(input.get(..6)) + .map_or(false, |(ntfs_prefix, first_6_of_input)| { + first_6_of_input.eq_ignore_ascii_case(ntfs_prefix) + && input.get(6) == Some(&b'~') + // It's notable that only `~1` to `~4` are possible before the disambiguation algorithm + // switches to using the `ntfs_shortname_prefix`, which is checked hereafter. + && input.get(7).map_or(false, |num| (b'1'..=b'4').contains(num)) + }) + { + return is_done_ntfs(input.get(8..)); + } + + let ntfs_shortname_prefix: &[u8] = ntfs_shortname_prefix.as_bytes(); + let mut saw_tilde = false; + let mut pos = 0; + while pos < 8 { + let Some(b) = input.get(pos).copied() else { + return false; + }; + if saw_tilde { + if !b.is_ascii_digit() { + return false; + } + } else if b == b'~' { + saw_tilde = true; + pos += 1; + let Some(b) = input.get(pos).copied() else { + return false; + }; + if !(b'1'..=b'9').contains(&b) { + return false; + } + } else if pos >= 6 + || b & 0x80 == 0x80 + || ntfs_shortname_prefix + .get(pos) + .map_or(true, |ob| !b.eq_ignore_ascii_case(ob)) + { + return false; + } + pos += 1; + } + is_done_ntfs(input.get(pos..)) + } +} + +/// Check if trailing filename bytes leave a match to special files like `.git` unchanged in NTFS. +fn is_done_ntfs(input: Option<&[u8]>) -> bool { + // Skip spaces and dots. Then return true if we are at the end or a colon. + let Some(input) = input else { return true }; + for b in input.bytes() { + if b == b':' { + return true; + } + if b != b' ' && b != b'.' { + return false; + } + } + true +} + +/// Check if trailing filename bytes leave a match to Windows reserved device names unchanged. +fn is_done_windows(input: Option<&[u8]>) -> bool { + // Skip spaces. Then return true if we are at the end or a dot or colon. + let Some(input) = input else { return true }; + let skip = input.bytes().take_while(|b| *b == b' ').count(); + let Some(next) = input.get(skip) else { return true }; + *next == b'.' || *next == b':' +} diff --git a/gix-validate/tests/path/mod.rs b/gix-validate/tests/path/mod.rs new file mode 100644 index 00000000000..e9aa7a6ec88 --- /dev/null +++ b/gix-validate/tests/path/mod.rs @@ -0,0 +1,350 @@ +#[test] +fn component_is_windows_device() { + for device in ["con", "CONIN$", "lpt1.txt", "AUX", "Prn", "NUL", "COM9"] { + assert!(gix_validate::path::component_is_windows_device(device.into())); + } + for not_device in ["coni", "CONIN", "lpt", "AUXi", "aPrn", "NULl", "COM"] { + assert!(!gix_validate::path::component_is_windows_device(not_device.into())); + } +} + +mod component { + use gix_validate::path::component; + + const NO_OPTS: component::Options = component::Options { + protect_windows: false, + protect_hfs: false, + protect_ntfs: false, + }; + const ALL_OPTS: component::Options = component::Options { + protect_windows: true, + protect_hfs: true, + protect_ntfs: true, + }; + + mod valid { + use crate::path::component::{ALL_OPTS, NO_OPTS}; + use bstr::ByteSlice; + use gix_validate::path::component; + use gix_validate::path::component::Mode::Symlink; + macro_rules! mktest { + ($name:ident, $input:expr) => { + mktest!($name, $input, ALL_OPTS); + }; + ($name:ident, $input:expr, $opts:expr) => { + #[test] + fn $name() { + assert!(gix_validate::path::component($input.as_bstr(), None, $opts).is_ok()) + } + }; + ($name:ident, $input:expr, $mode:expr, $opts:expr) => { + #[test] + fn $name() { + assert!(gix_validate::path::component($input.as_bstr(), Some($mode), $opts).is_ok()) + } + }; + } + + const UNIX_OPTS: component::Options = component::Options { + protect_windows: false, + protect_hfs: true, + protect_ntfs: true, + }; + + mktest!(ascii, b"ascii-only_and-that"); + mktest!(unicode, "😁👍👌".as_bytes()); + mktest!(backslashes_on_unix, b"\\", UNIX_OPTS); + mktest!(drive_letters_on_unix, b"c:", UNIX_OPTS); + mktest!(virtual_drive_letters_on_unix, "֍:".as_bytes(), UNIX_OPTS); + mktest!(unc_path_on_unix, b"\\\\?\\pictures", UNIX_OPTS); + mktest!(not_dot_git_longer, b".gitu", NO_OPTS); + mktest!(not_dot_git_longer_all, b".gitu"); + mktest!(not_dot_gitmodules_shorter, b".gitmodule", Symlink, NO_OPTS); + mktest!(not_dot_gitmodules_shorter_all, b".gitmodule", Symlink, ALL_OPTS); + mktest!(not_dot_gitmodules_longer, b".gitmodulesa", Symlink, NO_OPTS); + mktest!(not_dot_gitmodules_longer_all, b".gitmodulesa", Symlink, ALL_OPTS); + mktest!(dot_gitmodules_as_file, b".gitmodules", UNIX_OPTS); + mktest!( + starts_with_dot_git_with_backslashes_on_linux, + b".git\\hooks\\pre-commit", + UNIX_OPTS + ); + mktest!(not_dot_git_shorter, b".gi", NO_OPTS); + mktest!(not_dot_git_shorter_ntfs_8_3, b"gi~1"); + mktest!(not_dot_git_longer_ntfs_8_3, b"gitu~1"); + mktest!(not_dot_git_shorter_ntfs_8_3_disabled, b"git~1", NO_OPTS); + mktest!(not_dot_git_longer_hfs, ".g\u{200c}itu".as_bytes()); + mktest!(not_dot_git_shorter_hfs, ".g\u{200c}i".as_bytes()); + mktest!(com_0_lower, b"com0"); + mktest!(com_without_number_0_lower, b"comm"); + mktest!(conout_without_dollar_with_extension, b"conout.c"); + mktest!(conin_without_dollar_with_extension, b"conin.c"); + mktest!(conin_without_dollar, b"conin"); + mktest!(not_con, b"com"); + mktest!(also_not_con, b"co"); + mktest!(not_nul, b"null"); + mktest!( + not_dot_gitmodules_shorter_hfs, + ".gitm\u{200c}odule".as_bytes(), + Symlink, + UNIX_OPTS + ); + mktest!(dot_gitmodules_as_file_hfs, ".g\u{200c}itmodules".as_bytes(), UNIX_OPTS); + mktest!(dot_gitmodules_ntfs_8_3_disabled, b"gItMOD~1", Symlink, NO_OPTS); + mktest!( + not_dot_gitmodules_longer_hfs, + "\u{200c}.gitmodulesa".as_bytes(), + Symlink, + UNIX_OPTS + ); + } + + mod invalid { + use crate::path::component::{ALL_OPTS, NO_OPTS}; + use bstr::ByteSlice; + use gix_validate::path::component::Error; + use gix_validate::path::component::Mode::Symlink; + + macro_rules! mktest { + ($name:ident, $input:expr, $expected:pat) => { + mktest!($name, $input, $expected, ALL_OPTS); + }; + ($name:ident, $input:expr, $expected:pat, $opts:expr) => { + #[test] + fn $name() { + match gix_validate::path::component($input.as_bstr(), None, $opts) { + Err($expected) => {} + got => panic!("Wanted {}, got {:?}", stringify!($expected), got), + } + } + }; + ($name:ident, $input:expr, $expected:pat, $mode:expr, $opts:expr) => { + #[test] + fn $name() { + match gix_validate::path::component($input.as_bstr(), Some($mode), $opts) { + Err($expected) => {} + got => panic!("Wanted {}, got {:?}", stringify!($expected), got), + } + } + }; + } + + mktest!(empty, b"", Error::Empty); + mktest!(dot_git_lower, b".git", Error::DotGitDir, NO_OPTS); + mktest!(dot_git_lower_hfs, ".g\u{200c}it".as_bytes(), Error::DotGitDir); + mktest!(dot_git_mixed_hfs_simple, b".Git", Error::DotGitDir); + mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS); + mktest!( + starts_with_dot_git_with_backslashes_on_windows, + b".git\\hooks\\pre-commit", + Error::PathSeparator + ); + mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir); + mktest!(dot_git_upper_ntfs_8_3, b"GIT~1", Error::DotGitDir); + mktest!(dot_git_mixed, b".gIt", Error::DotGitDir, NO_OPTS); + mktest!(dot_git_mixed_ntfs_8_3, b"gIt~1", Error::DotGitDir); + mktest!( + dot_gitmodules_mixed, + b".gItmodules", + Error::SymlinkedGitModules, + Symlink, + NO_OPTS + ); + mktest!(dot_git_mixed_hfs, "\u{206e}.gIt".as_bytes(), Error::DotGitDir); + mktest!( + dot_git_ntfs_8_3_numbers_only, + b"~1000000", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_git_ntfs_8_3_numbers_only_too, + b"~9999999", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_hfs, + "\u{206e}.gItmodules".as_bytes(), + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_ntfs_8_3, + b"gItMOD~1", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_mixed_ntfs_stream, + b".giTmodUles:$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + dot_gitmodules_lower_ntfs_stream_default_implicit, + b".gitmodules::$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + ntfs_stream_default_implicit, + b"file::$DATA", + Error::WindowsIllegalCharacter + ); + mktest!( + ntfs_stream_explicit, + b"file:ANYTHING_REALLY:$DATA", + Error::WindowsIllegalCharacter + ); + mktest!( + dot_gitmodules_lower_ntfs_stream, + b".gitmodules:$DATA:$DATA", + Error::SymlinkedGitModules, + Symlink, + ALL_OPTS + ); + mktest!( + not_gitmodules_trailing_space, + b".gitmodules x ", + Error::WindowsIllegalCharacter + ); + mktest!( + not_gitmodules_trailing_stream, + b".gitmodules,:$DATA", + Error::WindowsIllegalCharacter + ); + mktest!(path_separator_slash_between, b"a/b", Error::PathSeparator); + mktest!(path_separator_slash_leading, b"/a", Error::PathSeparator); + mktest!(path_separator_slash_trailing, b"a/", Error::PathSeparator); + mktest!(path_separator_slash_only, b"/", Error::PathSeparator); + mktest!(slashes_on_windows, b"/", Error::PathSeparator, ALL_OPTS); + mktest!(backslashes_on_windows, b"\\", Error::PathSeparator, ALL_OPTS); + mktest!(path_separator_backslash_between, b"a\\b", Error::PathSeparator); + mktest!(path_separator_backslash_leading, b"\\a", Error::PathSeparator); + mktest!(path_separator_backslash_trailing, b"a\\", Error::PathSeparator); + mktest!(aux_mixed, b"Aux", Error::WindowsReservedName); + mktest!(aux_with_extension, b"aux.c", Error::WindowsReservedName); + mktest!(com_lower, b"com1", Error::WindowsReservedName); + mktest!(com_upper_with_extension, b"COM9.c", Error::WindowsReservedName); + mktest!(trailing_space, b"win32 ", Error::WindowsIllegalCharacter); + mktest!(trailing_dot, b"win32.", Error::WindowsIllegalCharacter); + mktest!(trailing_dot_dot, b"win32 . .", Error::WindowsIllegalCharacter); + mktest!(colon_inbetween, b"colon:separates", Error::WindowsIllegalCharacter); + mktest!(left_arrow, b"arrowright", Error::WindowsIllegalCharacter); + mktest!(apostrophe, b"a\"b", Error::WindowsIllegalCharacter); + mktest!(pipe, b"a|b", Error::WindowsIllegalCharacter); + mktest!(questionmark, b"a?b", Error::WindowsIllegalCharacter); + mktest!(asterisk, b"a*b", Error::WindowsIllegalCharacter); + mktest!(lpt_mixed_with_number, b"LPt8", Error::WindowsReservedName); + mktest!(nul_mixed, b"NuL", Error::WindowsReservedName); + mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName); + mktest!(con, b"CON", Error::WindowsReservedName); + mktest!(con_with_extension, b"CON.abc", Error::WindowsReservedName); + mktest!( + conout_mixed_with_extension, + b"ConOut$ .xyz", + Error::WindowsReservedName + ); + mktest!(conin_mixed, b"conIn$ ", Error::WindowsReservedName); + mktest!(drive_letters, b"c:", Error::WindowsPathPrefix, ALL_OPTS); + mktest!( + virtual_drive_letters, + "֍:".as_bytes(), + Error::WindowsPathPrefix, + ALL_OPTS + ); + mktest!(unc_path, b"\\\\?\\pictures", Error::PathSeparator, ALL_OPTS); + + #[test] + fn ntfs_gitmodules() { + for invalid in [ + ".gitmodules", + ".Gitmodules", + ".gitmoduleS", + ".gitmodules ", + ".gitmodules.", + ".gitmodules ", + ".gitmodules. ", + ".gitmodules .", + ".gitmodules..", + ".gitmodules ", + ".gitmodules. ", + ".gitmodules . ", + ".gitmodules .", + ".Gitmodules ", + ".Gitmodules.", + ".Gitmodules ", + ".Gitmodules. ", + ".Gitmodules .", + ".Gitmodules..", + ".Gitmodules ", + ".Gitmodules. ", + ".Gitmodules . ", + ".Gitmodules .", + "GITMOD~1", + "gitmod~1", + "GITMOD~2", + "giTmod~3", + "GITMOD~4", + "GITMOD~1 ", + "gitMod~2.", + "GITMOD~3 ", + "gitmod~4. ", + "GITMoD~1 .", + "gitmod~2 ", + "GITMOD~3. ", + "gitmoD~4 . ", + "GI7EBA~1", + "gi7eba~9", + "GI7EB~10", + "GI7EB~11", + "GI7EB~99", + "GI7EB~10", + "GI7E~100", + "GI7E~101", + "GI7E~999", + ".gitmodules:$DATA", + "gitmod~4 . :$DATA", + ] { + match gix_validate::path::component(invalid.into(), Some(Symlink), ALL_OPTS) { + Ok(_) => { + unreachable!("{invalid:?} should not validate successfully") + } + Err(err) => { + assert!(matches!(err, Error::SymlinkedGitModules)) + } + } + } + + for valid in [ + ".gitmodules x", + ".gitmodules .x", + " .gitmodules", + "..gitmodules", + "gitmodules", + ".gitmodule", + ".gitmodules .x", + "GI7EBA~", + "GI7EBA~0", + "GI7EBA~~1", + "GI7EBA~X", + "Gx7EBA~1", + "GI7EBX~1", + "GI7EB~1", + "GI7EB~01", + "GI7EB~1X", + ] { + gix_validate::path::component(valid.into(), Some(Symlink), ALL_OPTS) + .unwrap_or_else(|_| panic!("{valid:?} should have been valid")); + } + } + } +} diff --git a/gix-validate/tests/validate.rs b/gix-validate/tests/validate.rs index db45c4aac56..d1951c3d159 100644 --- a/gix-validate/tests/validate.rs +++ b/gix-validate/tests/validate.rs @@ -1,3 +1,4 @@ +mod path; mod reference; mod submodule; mod tag; diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index 77db18daa1e..b08563c60c1 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -80,8 +80,7 @@ where let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 { path: entry_path.to_owned(), })?; - let is_dir = Some(entry.mode == gix_index::entry::Mode::COMMIT || entry.mode == gix_index::entry::Mode::DIR); - let path_cache = path_cache.at_path(dest_relative, is_dir, &*objects)?; + let path_cache = path_cache.at_path(dest_relative, Some(entry.mode), &*objects)?; let dest = path_cache.path(); let object_size = match entry.mode { diff --git a/gix-worktree-state/src/checkout/function.rs b/gix-worktree-state/src/checkout/function.rs index 9046af47110..6342f34cd59 100644 --- a/gix-worktree-state/src/checkout/function.rs +++ b/gix-worktree-state/src/checkout/function.rs @@ -64,7 +64,11 @@ where path_cache: Stack::from_state_and_ignore_case( dir, options.fs.ignore_case, - stack::State::for_checkout(options.overwrite_existing, std::mem::take(&mut options.attributes)), + stack::State::for_checkout( + options.overwrite_existing, + options.validate, + std::mem::take(&mut options.attributes), + ), index, paths, ), diff --git a/gix-worktree-state/src/checkout/mod.rs b/gix-worktree-state/src/checkout/mod.rs index 0206e7e3408..57a0d3b5f2a 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -41,6 +41,8 @@ pub struct Outcome { pub struct Options { /// capabilities of the file system pub fs: gix_fs::Capabilities, + /// Options to configure how to validate path components. + pub validate: gix_worktree::validate::path::component::Options, /// If set, don't use more than this amount of threads. /// Otherwise, usually use as many threads as there are logical cores. /// A value of 0 is interpreted as no-limit diff --git a/gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz b/gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz new file mode 100644 index 00000000000..930081b86fe Binary files /dev/null and b/gix-worktree-state/tests/fixtures/generated-archives/make_traverse_trees.tar.xz differ diff --git a/gix-worktree-state/tests/fixtures/make_traverse_trees.sh b/gix-worktree-state/tests/fixtures/make_traverse_trees.sh new file mode 100755 index 00000000000..0dd59db0da1 --- /dev/null +++ b/gix-worktree-state/tests/fixtures/make_traverse_trees.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eu -o pipefail + +# Makes a repo carrying a tree structure representing the given path to a blob. +# File content is from stdin. Args are repo name, path, -x or +x, and tr sets. +function make_repo() ( + local repo="$1" path="$2" xbit="$3" set1="$4" set2="$5" + local dir dir_standin path_standin path_standin_pattern path_replacement + + git init -- "$repo" + cd -- "$repo" # Temporary, as the function body is a ( ) subshell. + + dir="${path%/*}" + dir_standin="$(tr "$set1" "$set2" <<<"$dir")" + path_standin="$(tr "$set1" "$set2" <<<"$path")" + mkdir -p -- "$dir_standin" + cat >"$path_standin" + git add --chmod="$xbit" -- "$path_standin" + path_standin_pattern="$(sed 's/[|.*^$\]/\\&/g' <<<"$path_standin")" + path_replacement="$(sed 's/[|&\]/\\&/g' <<<"$path")" + cp .git/index old_index + LC_ALL=C sed "s|$path_standin_pattern|$path_replacement|g" old_index >.git/index + git commit -m 'Initial commit' +) + +make_repo traverse_dotdot_trees '../outside' -x '.' '@' \ + <<<'A file outside the working tree, somehow.' + +make_repo traverse_dotgit_trees '.git/hooks/pre-commit' +x '.' '@' <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF + +make_repo traverse_dotgit_stream '.git::$INDEX_ALLOCATION/hooks/pre-commit' +x ':' ',' <<'EOF' +#!/bin/sh +printf 'Vulnerable!\n' +date >vulnerable +EOF diff --git a/gix-worktree-state/tests/state/checkout.rs b/gix-worktree-state/tests/state/checkout.rs index c38a81877af..2a852ce7165 100644 --- a/gix-worktree-state/tests/state/checkout.rs +++ b/gix-worktree-state/tests/state/checkout.rs @@ -51,7 +51,7 @@ fn assure_is_empty(dir: impl AsRef) -> std::io::Result<()> { fn submodules_are_instantiated_as_directories() -> crate::Result { let mut opts = opts_from_probe(); opts.overwrite_existing = false; - let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir(opts.clone(), "make_mixed")?; + let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir(opts.clone(), "make_mixed", None)?; for path in ["m1", "modules/m1"] { let sm = destination.path().join(path); @@ -68,7 +68,7 @@ fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden( // without overwrite mode, everything is safe. opts.overwrite_existing = false; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink", None).unwrap(); let source_files = dir_structure(&source_tree); let worktree_files = dir_structure(&destination); @@ -109,7 +109,7 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { // with overwrite mode opts.overwrite_existing = true; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink", None).unwrap(); let source_files = dir_structure(&source_tree); let worktree_files = dir_structure(&destination); @@ -144,8 +144,13 @@ fn delayed_driver_process() -> crate::Result { opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; opts.destination_is_initially_empty = false; setup_filter_pipeline(opts.filters.options_mut()); - let (_source, destination, _index, outcome) = - checkout_index_in_tmp_dir_opts(opts, "make_mixed_without_submodules_and_symlinks", |_| true, |_| Ok(()))?; + let (_source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( + opts, + "make_mixed_without_submodules_and_symlinks", + None, + |_| true, + |_| Ok(()), + )?; assert_eq!(outcome.collisions.len(), 0); assert_eq!(outcome.errors.len(), 0); assert_eq!(outcome.files_updated, 5); @@ -178,6 +183,10 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { gix_filter::driver::apply::Delay::Forbid, ] { let mut opts = opts_from_probe(); + assert!( + opts.fs.symlink, + "BUG: the probe must detect to be able to generate symlinks" + ); opts.overwrite_existing = true; opts.filter_process_delay = delay; opts.destination_is_initially_empty = false; @@ -185,6 +194,7 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { let (source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( opts.clone(), "make_mixed", + None, |_| true, |d| { let empty = d.join("empty"); @@ -197,7 +207,7 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result { let dir = dir.join("sub-dir"); std::fs::create_dir(&dir)?; - symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to another file + symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to a directory. Ok(()) }, )?; @@ -250,7 +260,7 @@ fn symlinks_become_files_if_disabled() -> crate::Result { let mut opts = opts_from_probe(); opts.fs.symlink = false; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules", None)?; assert_equality(&source_tree, &destination, opts.fs.symlink)?; assert!(outcome.collisions.is_empty()); @@ -266,7 +276,7 @@ fn dangling_symlinks_can_be_created() -> crate::Result { } let (_source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_dangling_symlink")?; + checkout_index_in_tmp_dir(opts.clone(), "make_dangling_symlink", None)?; let worktree_files = dir_structure(&destination); let worktree_files_stripped = stripped_prefix(&destination, &worktree_files); @@ -287,7 +297,7 @@ fn allow_or_disallow_symlinks() -> crate::Result { for allowed in &[false, true] { opts.fs.symlink = *allowed; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules", None)?; assert_equality(&source_tree, &destination, opts.fs.symlink)?; assert!(outcome.collisions.is_empty()); @@ -303,6 +313,7 @@ fn keep_going_collects_results() { let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( opts, "make_mixed_without_submodules", + None, |_id| { count .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { @@ -365,7 +376,7 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { return; } let (source_tree, destination, index, outcome) = - checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions").unwrap(); + checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions", None).unwrap(); assert!(outcome.collisions.is_empty()); let num_files = assert_equality(&source_tree, &destination, opts.fs.symlink).unwrap(); @@ -380,6 +391,48 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { ); } +#[test] +fn safety_checks_dotdot_trees() { + let mut opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotdot_trees")).unwrap_err(); + let expected_err_msg = "Input path \"../outside\" contains relative or absolute components"; + assert_eq!(err.source().expect("inner").to_string(), expected_err_msg); + + opts.keep_going = true; + let (_source_tree, _destination, _index, outcome) = + checkout_index_in_tmp_dir(opts, "make_traverse_trees", Some("traverse_dotdot_trees")) + .expect("keep-going checks out as much as possible"); + assert_eq!(outcome.errors.len(), 1, "one path could not be checked out"); + assert_eq!( + outcome.errors[0].error.source().expect("inner").to_string(), + expected_err_msg + ); +} + +#[test] +fn safety_checks_dotgit_trees() { + let opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_trees")).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "The .git name may never be used" + ); +} + +#[test] +fn safety_checks_dotgit_ntfs_stream() { + let opts = opts_from_probe(); + let err = + checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_stream")).unwrap_err(); + assert_eq!( + err.source().expect("inner").to_string(), + "The .git name may never be used", + "note how it is still discovered even though the path is `.git::$INDEX_ALLOCATION`" + ); +} + #[test] fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_filters() { let mut opts = opts_from_probe(); @@ -390,7 +443,7 @@ fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_fi setup_filter_pipeline(opts.filters.options_mut()); opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; let (source_tree, destination, _index, outcome) = - checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); + checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions", None).unwrap(); let source_files = dir_structure(&source_tree); assert_eq!( @@ -502,17 +555,26 @@ pub fn dir_structure>(path: P) -> Vec, ) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree_state::checkout::Outcome)> { - checkout_index_in_tmp_dir_opts(opts, name, |_d| true, |_| Ok(())) + checkout_index_in_tmp_dir_opts(opts, name, subdir_name, |_d| true, |_| Ok(())) } fn checkout_index_in_tmp_dir_opts( opts: gix_worktree_state::checkout::Options, - name: &str, + script_name: &str, + subdir_name: Option<&str>, allow_return_object: impl FnMut(&gix_hash::oid) -> bool + Send + Clone, prep_dest: impl Fn(&Path) -> std::io::Result<()>, ) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree_state::checkout::Outcome)> { - let source_tree = fixture_path(name); + let source_tree = { + let root = fixture_path(script_name); + if let Some(name) = subdir_name { + root.join(name) + } else { + root + } + }; let git_dir = source_tree.join(".git"); let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; let odb = gix_odb::at(git_dir.join("objects"))?.into_inner().into_arc()?; diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 3c2964e1918..56a211d5600 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -67,7 +67,7 @@ mod from_tree { mutating_pipeline(true), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, @@ -225,7 +225,7 @@ mod from_tree { mutating_pipeline(false), move |rela_path, mode, attrs| { cache - .at_entry(rela_path, mode.is_tree().into(), &odb) + .at_entry(rela_path, Some(mode.into()), &odb) .map(|entry| entry.matching_attributes(attrs)) .map(|_| ()) }, diff --git a/gix-worktree/Cargo.toml b/gix-worktree/Cargo.toml index a01fbb40f27..cf691531416 100644 --- a/gix-worktree/Cargo.toml +++ b/gix-worktree/Cargo.toml @@ -16,9 +16,9 @@ doctest = false [features] default = ["attributes"] ## Instantiate stacks that can access `.gitattributes` information. -attributes = ["dep:gix-attributes"] +attributes = ["dep:gix-attributes", "dep:gix-validate"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = [ "dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-object/serde", "gix-attributes?/serde", "gix-ignore/serde" ] +serde = ["dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-object/serde", "gix-attributes?/serde", "gix-ignore/serde"] [dependencies] gix-index = { version = "^0.32.1", path = "../gix-index" } @@ -28,10 +28,11 @@ gix-object = { version = "^0.42.0", path = "../gix-object" } gix-glob = { version = "^0.16.2", path = "../gix-glob" } gix-path = { version = "^0.10.7", path = "../gix-path" } gix-attributes = { version = "^0.22.2", path = "../gix-attributes", optional = true } +gix-validate = { version = "^0.8.4", path = "../gix-validate", optional = true } gix-ignore = { version = "^0.11.2", path = "../gix-ignore" } gix-features = { version = "^0.38.1", path = "../gix-features" } -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} +serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } bstr = { version = "1.3.0", default-features = false } document-features = { version = "0.2.0", optional = true } diff --git a/gix-worktree/src/lib.rs b/gix-worktree/src/lib.rs index 7238538e73d..a68bea99826 100644 --- a/gix-worktree/src/lib.rs +++ b/gix-worktree/src/lib.rs @@ -19,6 +19,9 @@ pub use gix_glob as glob; pub use gix_ignore as ignore; /// Provides types needed for using [`Stack::at_path()`] and [`Stack::at_entry()`]. pub use gix_object as object; +/// Provides types needed for using [`stack::State::for_checkout()`]. +#[cfg(feature = "attributes")] +pub use gix_validate as validate; /// A cache for efficiently executing operations on directories and files which are encountered in sorted order. /// That way, these operations can be re-used for subsequent invocations in the same directory. diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index 1234346c5de..d2b3a011b0b 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -1,5 +1,4 @@ -use bstr::{BStr, ByteSlice}; - +use crate::stack::mode_is_dir; use crate::{stack::State, PathIdMapping}; /// Various aggregate numbers related to the stack delegate itself. @@ -22,7 +21,7 @@ pub(crate) struct StackDelegate<'a, 'find> { pub state: &'a mut State, pub buf: &'a mut Vec, #[cfg_attr(not(feature = "attributes"), allow(dead_code))] - pub is_dir: bool, + pub mode: Option, pub id_mappings: &'a Vec, pub objects: &'find dyn gix_object::Find, pub case: gix_glob::pattern::Case, @@ -32,29 +31,15 @@ pub(crate) struct StackDelegate<'a, 'find> { impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { self.statistics.delegate.push_directory += 1; - let dir_bstr = gix_path::into_bstr(stack.current()); - let rela_dir_cow = gix_path::to_unix_separators_on_windows( - gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( - gix_path::into_bstr(stack.root()).as_ref(), - dir_bstr.as_ref(), - None, - self.case, - ) - .expect("dir in root") - .0, - ); - let rela_dir: &BStr = if rela_dir_cow.starts_with(b"/") { - rela_dir_cow[1..].as_bstr() - } else { - rela_dir_cow.as_ref() - }; + let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()); + let rela_dir = gix_path::to_unix_separators_on_windows(rela_dir_bstr); match &mut self.state { #[cfg(feature = "attributes")] State::CreateDirectoryAndAttributesStack { attributes, .. } | State::AttributesStack(attributes) => { attributes.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -66,7 +51,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { attributes.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -75,7 +60,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { ignore.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -86,7 +71,7 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { State::IgnoreStack(ignore) => ignore.push_directory( stack.root(), stack.current(), - rela_dir, + &rela_dir, self.buf, self.id_mappings, self.objects, @@ -104,14 +89,18 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { #[cfg(feature = "attributes")] State::CreateDirectoryAndAttributesStack { unlink_on_collision, + validate, attributes: _, - } => create_leading_directory( - is_last_component, - stack, - self.is_dir, - &mut self.statistics.delegate.num_mkdir_calls, - *unlink_on_collision, - )?, + } => { + validate_last_component(stack, self.mode, *validate)?; + create_leading_directory( + is_last_component, + stack, + self.mode, + &mut self.statistics.delegate.num_mkdir_calls, + *unlink_on_collision, + )? + } #[cfg(feature = "attributes")] State::AttributesAndIgnoreStack { .. } | State::AttributesStack(_) => {} State::IgnoreStack(_) => {} @@ -138,15 +127,47 @@ impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { } } +#[cfg(feature = "attributes")] +fn validate_last_component( + stack: &gix_fs::Stack, + mode: Option, + opts: gix_validate::path::component::Options, +) -> std::io::Result<()> { + let Some(last_component) = stack.current_relative().components().next_back() else { + return Ok(()); + }; + let last_component = + gix_path::try_into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref())).map_err(|_err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated", + stack.current_relative().display() + ), + ) + })?; + + if let Err(err) = gix_validate::path::component( + last_component.as_ref(), + mode.and_then(|m| { + (m == gix_index::entry::Mode::SYMLINK).then_some(gix_validate::path::component::Mode::Symlink) + }), + opts, + ) { + return Err(std::io::Error::new(std::io::ErrorKind::Other, err)); + } + Ok(()) +} + #[cfg(feature = "attributes")] fn create_leading_directory( is_last_component: bool, stack: &gix_fs::Stack, - is_dir: bool, + mode: Option, mkdir_calls: &mut usize, unlink_on_collision: bool, ) -> std::io::Result<()> { - if is_last_component && !is_dir { + if is_last_component && !mode_is_dir(mode).unwrap_or(false) { return Ok(()); } *mkdir_calls += 1; diff --git a/gix-worktree/src/stack/mod.rs b/gix-worktree/src/stack/mod.rs index 4629a7a08ed..1e321650ca7 100644 --- a/gix-worktree/src/stack/mod.rs +++ b/gix-worktree/src/stack/mod.rs @@ -28,6 +28,8 @@ pub enum State { CreateDirectoryAndAttributesStack { /// If there is a symlink or a file in our path, try to unlink it before creating the directory. unlink_on_collision: bool, + /// Options to control how newly created path components should be validated. + validate: gix_validate::path::component::Options, /// State to handle attribute information attributes: state::Attributes, }, @@ -103,22 +105,23 @@ impl Stack { impl Stack { /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no /// symlinks are in that path. - /// Unless `is_dir` is known with `Some(…)`, then `relative` points to a directory itself in which case the entire resulting - /// path is created as directory. If it's not known it is assumed to be a file. + /// Unless `mode` is known with `Some(gix_index::entry::Mode::DIR|COMMIT)`, + /// then `relative` points to a directory itself in which case the entire resulting path is created as directory. + /// If it's not known it is assumed to be a file. /// `objects` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()], with mappnigs /// /// Provide access to cached information for that `relative` path via the returned platform. pub fn at_path( &mut self, relative: impl AsRef, - is_dir: Option, + mode: Option, objects: &dyn gix_object::Find, ) -> std::io::Result> { self.statistics.platforms += 1; let mut delegate = StackDelegate { state: &mut self.state, buf: &mut self.buf, - is_dir: is_dir.unwrap_or(false), + mode, id_mappings: &self.id_mappings, objects, case: self.case, @@ -126,36 +129,46 @@ impl Stack { }; self.stack .make_relative_path_current(relative.as_ref(), &mut delegate)?; - Ok(Platform { parent: self, is_dir }) + Ok(Platform { + parent: self, + is_dir: mode_is_dir(mode), + }) } - /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `is_dir` should reflect - /// whether it's a directory or not, or left at `None` if unknown. + /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `mode` should reflect + /// the kind of item set here, or left at `None` if unknown. /// `objects` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()]. /// All effects are similar to [`at_path()`][Self::at_path()]. /// - /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. - /// - /// ### Panics - /// - /// on illformed UTF8 in `relative` + /// If `relative` ends with `/` and `mode` is `None`, it is automatically assumed set to be a directory. pub fn at_entry<'r>( &mut self, relative: impl Into<&'r BStr>, - is_dir: Option, + mode: Option, objects: &dyn gix_object::Find, ) -> std::io::Result> { let relative = relative.into(); - let relative_path = gix_path::from_bstr(relative); + let relative_path = gix_path::try_from_bstr(relative).map_err(|_err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("The path \"{relative}\" contained invalid UTF-8 and could not be turned into a path"), + ) + })?; self.at_path( relative_path, - is_dir.or_else(|| relative.ends_with_str("/").then_some(true)), + mode.or_else(|| relative.ends_with_str("/").then_some(gix_index::entry::Mode::DIR)), objects, ) } } +fn mode_is_dir(mode: Option) -> Option { + mode.map(|m| + // This applies to directories and commits (submodules are directories on disk) + m.is_sparse() || m.is_submodule()) +} + /// Mutation impl Stack { /// Reset the statistics after returning them. diff --git a/gix-worktree/src/stack/state/attributes.rs b/gix-worktree/src/stack/state/attributes.rs index 04ad8b5c712..1071e4a9f27 100644 --- a/gix-worktree/src/stack/state/attributes.rs +++ b/gix-worktree/src/stack/state/attributes.rs @@ -98,8 +98,7 @@ impl Attributes { objects: &dyn gix_object::Find, stats: &mut Statistics, ) -> std::io::Result<()> { - let attr_path_relative = - gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); + let attr_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes"); let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); // Git does not follow symbolic links as per documentation. let no_follow_symlinks = false; diff --git a/gix-worktree/src/stack/state/mod.rs b/gix-worktree/src/stack/state/mod.rs index 04afb046368..52d74daac61 100644 --- a/gix-worktree/src/stack/state/mod.rs +++ b/gix-worktree/src/stack/state/mod.rs @@ -60,9 +60,14 @@ pub mod ignore; impl State { /// Configure a state to be suitable for checking out files, which only needs access to attribute files read from the index. #[cfg(feature = "attributes")] - pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { + pub fn for_checkout( + unlink_on_collision: bool, + validate: gix_validate::path::component::Options, + attributes: Attributes, + ) -> Self { State::CreateDirectoryAndAttributesStack { unlink_on_collision, + validate, attributes, } } diff --git a/gix-worktree/tests/worktree/stack/attributes.rs b/gix-worktree/tests/worktree/stack/attributes.rs index ae5d7a6b542..8a2079eb011 100644 --- a/gix-worktree/tests/worktree/stack/attributes.rs +++ b/gix-worktree/tests/worktree/stack/attributes.rs @@ -17,6 +17,11 @@ fn baseline() -> crate::Result { let mut collection = gix_attributes::search::MetadataCollection::default(); let state = gix_worktree::stack::State::for_checkout( false, + gix_worktree::validate::path::component::Options { + protect_windows: false, + protect_ntfs: false, + ..Default::default() + }, state::Attributes::new( gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, Some(git_dir.join("info").join("attributes")), diff --git a/gix-worktree/tests/worktree/stack/create_directory.rs b/gix-worktree/tests/worktree/stack/create_directory.rs index 19130d6559f..65b4b04fd7d 100644 --- a/gix-worktree/tests/worktree/stack/create_directory.rs +++ b/gix-worktree/tests/worktree/stack/create_directory.rs @@ -3,19 +3,22 @@ use std::path::Path; use gix_testtools::tempfile::{tempdir, TempDir}; use gix_worktree::{stack, Stack}; +const IS_FILE: Option = Some(gix_index::entry::Mode::FILE); +const IS_DIR: Option = Some(gix_index::entry::Mode::DIR); + #[test] fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result { let dir = tempdir()?; let mut cache = Stack::new( dir.path().join("non-existing-root"), - stack::State::for_checkout(false, Default::default()), + stack::State::for_checkout(false, Default::default(), Default::default()), Default::default(), Vec::new(), Default::default(), ); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); - let path = cache.at_path("hello", Some(false), &gix_object::find::Never)?.path(); + let path = cache.at_path("hello", IS_FILE, &gix_object::find::Never)?.path(); assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); Ok(()) @@ -25,15 +28,15 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate fn directory_paths_are_created_in_full() { let (mut cache, _tmp) = new_cache(); - for (name, is_dir) in &[ - ("dir", Some(true)), - ("submodule", Some(true)), - ("file", Some(false)), - ("exe", Some(false)), + for (name, mode) in [ + ("dir", IS_DIR), + ("submodule", IS_DIR), + ("file", IS_FILE), + ("exe", IS_FILE), ("link", None), ] { let path = cache - .at_path(Path::new("dir").join(name), *is_dir, &gix_object::find::Never) + .at_path(Path::new("dir").join(name), mode, &gix_object::find::Never) .unwrap() .path(); assert!(path.parent().unwrap().is_dir(), "dir exists"); @@ -47,13 +50,30 @@ fn existing_directories_are_fine() -> crate::Result { let (mut cache, tmp) = new_cache(); std::fs::create_dir(tmp.path().join("dir"))?; - let path = cache.at_path("dir/file", Some(false), &gix_object::find::Never)?.path(); + let path = cache.at_path("dir/file", IS_FILE, &gix_object::find::Never)?.path(); assert!(path.parent().unwrap().is_dir(), "directory is still present"); assert!(!path.exists(), "it won't create the file"); assert_eq!(cache.statistics().delegate.num_mkdir_calls, 1); Ok(()) } +#[test] +fn validation_to_each_component() -> crate::Result { + let (mut cache, tmp) = new_cache(); + + let err = cache + .at_path("valid/.gIt", IS_FILE, &gix_object::find::Never) + .unwrap_err(); + assert_eq!( + cache.statistics().delegate.num_mkdir_calls, + 1, + "the valid directory was created" + ); + assert!(tmp.path().join("valid").is_dir(), "it was actually created"); + assert_eq!(err.to_string(), "The .git name may never be used"); + Ok(()) +} + #[test] fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::Result { let (mut cache, tmp) = new_cache(); @@ -72,7 +92,7 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R let relative_path = format!("{dirname}/file"); assert_eq!( cache - .at_path(&relative_path, Some(false), &gix_object::find::Never) + .at_path(&relative_path, IS_FILE, &gix_object::find::Never) .unwrap_err() .kind(), std::io::ErrorKind::AlreadyExists @@ -92,9 +112,7 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R *unlink_on_collision = true; } let relative_path = format!("{dirname}/file"); - let path = cache - .at_path(&relative_path, Some(false), &gix_object::find::Never)? - .path(); + let path = cache.at_path(&relative_path, IS_FILE, &gix_object::find::Never)?.path(); assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); assert!(!path.exists()); } @@ -110,7 +128,7 @@ fn new_cache() -> (Stack, TempDir) { let dir = tempdir().unwrap(); let cache = Stack::new( dir.path(), - stack::State::for_checkout(false, Default::default()), + stack::State::for_checkout(false, Default::default(), Default::default()), Default::default(), Vec::new(), Default::default(), diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index 6e578f352d8..7ea4fbcebe5 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -1,5 +1,7 @@ use bstr::{BStr, ByteSlice}; +use gix_index::entry::Mode; use gix_worktree::{stack::state::ignore::Source, Stack}; +use std::fs::Metadata; use crate::{hex_to_id, worktree::stack::probe_case}; @@ -62,7 +64,7 @@ fn exclude_by_dir_is_handled_just_like_git() { for (relative_entry, source_and_line) in expectations { let (source, line, expected_pattern) = source_and_line.expect("every value is matched"); let relative_path = gix_path::from_byte_slice(relative_entry); - let is_dir = dir.join(relative_path).metadata().ok().map(|m| m.is_dir()); + let is_dir = dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &FindError).unwrap(); let match_ = platform.matching_exclude_pattern().expect("match all values"); @@ -87,6 +89,14 @@ fn exclude_by_dir_is_handled_just_like_git() { } } +fn metadata_to_mode(meta: Metadata) -> Mode { + if meta.is_dir() { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} + #[test] fn check_against_baseline() -> crate::Result { let dir = gix_testtools::scripted_fixture_read_only_standalone("make_ignore_and_attributes_setup.sh")?; @@ -127,7 +137,7 @@ fn check_against_baseline() -> crate::Result { }; for (relative_entry, source_and_line) in expectations { let relative_path = gix_path::from_byte_slice(relative_entry); - let is_dir = worktree_dir.join(relative_path).metadata().ok().map(|m| m.is_dir()); + let is_dir = worktree_dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &odb)?; diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 556b64f2112..0eb88d0648b 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -382,7 +382,7 @@ gix-testtools = { path = "../tests/tools" } is_ci = "1.1.1" anyhow = "1" walkdir = "2.3.2" -serial_test = { version = "2.0.0", default-features = false } +serial_test = { version = "3.1.0", default-features = false } async-std = { version = "1.12.0", features = ["attributes"] } [package.metadata.docs.rs] diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index e2b9ecc5ce6..bf9a1cafb18 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -33,34 +33,29 @@ impl DerefMut for AttributeStack<'_> { /// Platform retrieval impl<'repo> AttributeStack<'repo> { - /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no - /// symlinks are in that path. - /// Unless `is_dir` is known with `Some(…)`, then `relative` points to a directory itself in which case the entire resulting - /// path is created as directory. If it's not known it is assumed to be a file. + /// Append the `relative` path to the root directory of the cache and load all attribute or ignore files on the way as needed. + /// Use `mode` to specify what kind of item lives at `relative` - directories may match against rules specifically. + /// If `mode` is `None`, the item at `relative` is assumed to be a file. /// - /// Provide access to cached information for that `relative` path via the returned platform. + /// The returned platform may be used to access the actual attribute or ignore information. #[doc(alias = "is_path_ignored", alias = "git2")] pub fn at_path( &mut self, relative: impl AsRef, - is_dir: Option, + mode: Option, ) -> std::io::Result> { - self.inner.at_path(relative, is_dir, &self.repo.objects) + self.inner.at_path(relative, mode, &self.repo.objects) } - /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `is_dir` should reflect - /// whether it's a directory or not, or left at `None` if unknown. + /// Obtain a platform for attribute or ignore lookups from a repo-`relative` path, typically obtained from an index entry. + /// `mode` should reflect whether it's a directory or not, or left at `None` if unknown. /// - /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. - /// - /// ### Panics - /// - /// - on illformed UTF8 in `relative` + /// If `relative` ends with `/` and `mode` is `None`, it is automatically assumed to be a directory. pub fn at_entry<'r>( &mut self, relative: impl Into<&'r BStr>, - is_dir: Option, + mode: Option, ) -> std::io::Result> { - self.inner.at_entry(relative, is_dir, &self.repo.objects) + self.inner.at_entry(relative, mode, &self.repo.objects) } } diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index e241adb18a1..e04a32fc484 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -18,8 +18,10 @@ pub mod main_worktree { #[error("Could not create index from tree at {id}")] IndexFromTree { id: gix_hash::ObjectId, - source: gix_traverse::tree::breadthfirst::Error, + source: gix_index::init::from_tree::Error, }, + #[error("Couldn't obtain configuration for core.protect*")] + BooleanConfig(#[from] crate::config::boolean::Error), #[error(transparent)] WriteIndex(#[from] gix_index::file::write::Error), #[error(transparent)] @@ -95,10 +97,11 @@ pub mod main_worktree { )) } }; - let index = gix_index::State::from_tree(&root_tree, &repo.objects).map_err(|err| Error::IndexFromTree { - id: root_tree, - source: err, - })?; + let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) + .map_err(|err| Error::IndexFromTree { + id: root_tree, + source: err, + })?; let mut index = gix_index::File::from_state(index, repo.index_path()); let mut opts = repo diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index f35699aa09d..d752dc169ce 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -271,6 +271,36 @@ impl Cache { }) } + #[cfg(feature = "index")] + pub(crate) fn protect_options(&self) -> Result { + const IS_WINDOWS: bool = cfg!(windows); + const IS_MACOS: bool = cfg!(target_os = "macos"); + const ALWAYS_ON_FOR_SAFETY: bool = true; + Ok(gix_validate::path::component::Options { + protect_windows: config::tree::gitoxide::Core::PROTECT_WINDOWS + .enrich_error( + self.resolved + .boolean("gitoxide", Some("core".into()), "protectWindows") + .unwrap_or(Ok(IS_WINDOWS)), + ) + .with_lenient_default_value(self.lenient_config, IS_WINDOWS)?, + protect_hfs: config::tree::Core::PROTECT_HFS + .enrich_error( + self.resolved + .boolean("core", None, "protectHFS") + .unwrap_or(Ok(IS_MACOS)), + ) + .with_lenient_default_value(self.lenient_config, IS_MACOS)?, + protect_ntfs: config::tree::Core::PROTECT_NTFS + .enrich_error( + self.resolved + .boolean("core", None, "protectNTFS") + .unwrap_or(Ok(ALWAYS_ON_FOR_SAFETY)), + ) + .with_lenient_default_value(self.lenient_config, ALWAYS_ON_FOR_SAFETY)?, + }) + } + /// Collect everything needed to checkout files into a worktree. /// Note that some of the options being returned will be defaulted so safe settings, the caller might have to override them /// depending on the use-case. @@ -310,6 +340,7 @@ impl Cache { }; Ok(gix_worktree_state::checkout::Options { filter_process_delay, + validate: self.protect_options()?, filters, attributes: self .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? diff --git a/gix/src/config/cache/incubate.rs b/gix/src/config/cache/incubate.rs index 0bd0a3b5df4..505bbfd0640 100644 --- a/gix/src/config/cache/incubate.rs +++ b/gix/src/config/cache/incubate.rs @@ -1,7 +1,7 @@ #![allow(clippy::result_large_err)] use super::{util, Error}; -use crate::config::cache::util::ApplyLeniency; +use crate::config::cache::util::{ApplyLeniency, ApplyLeniencyDefaultValue}; use crate::config::tree::{Core, Extensions, Key}; /// A utility to deal with the cyclic dependency between the ref store and the configuration. The ref-store needs the @@ -15,6 +15,7 @@ pub(crate) struct StageOne { pub object_hash: gix_hash::Kind, pub reflog: Option, pub precompose_unicode: bool, + pub protect_windows: bool, } /// Initialization @@ -80,6 +81,15 @@ impl StageOne { .map_err(Error::ConfigBoolean)? .unwrap_or_default(); + const IS_WINDOWS: bool = cfg!(windows); + let protect_windows = crate::config::tree::gitoxide::Core::PROTECT_WINDOWS + .enrich_error( + config + .boolean("gitoxide", Some("core".into()), "protectWindows") + .unwrap_or(Ok(IS_WINDOWS)), + ) + .with_lenient_default_value(lenient, IS_WINDOWS)?; + let reflog = util::query_refupdates(&config, lenient)?; Ok(StageOne { git_dir_config: config, @@ -89,6 +99,7 @@ impl StageOne { object_hash, reflog, precompose_unicode, + protect_windows, }) } } diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 76e6dd81e41..56eb50e7059 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -28,6 +28,7 @@ impl Cache { object_hash, reflog: _, precompose_unicode: _, + protect_windows: _, }: StageOne, git_dir: &std::path::Path, branch_name: Option<&gix_ref::FullNameRef>, diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index 5a63020b11c..d847b4ed75f 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -44,6 +44,10 @@ impl Core { /// Needs application to use [`env::args_os`][crate::env::args_os()] to conform all input paths before they are used. pub const PRECOMPOSE_UNICODE: keys::Boolean = keys::Boolean::new_boolean("precomposeUnicode", &config::Tree::CORE) .with_note("application needs to conform all program input by using gix::env::args_os()"); + /// The `core.protectHFS` key. + pub const PROTECT_HFS: keys::Boolean = keys::Boolean::new_boolean("protectHFS", &config::Tree::CORE); + /// The `core.protectNTFS` key. + pub const PROTECT_NTFS: keys::Boolean = keys::Boolean::new_boolean("protectNTFS", &config::Tree::CORE); /// The `core.repositoryFormatVersion` key. pub const REPOSITORY_FORMAT_VERSION: keys::UnsignedInteger = keys::UnsignedInteger::new_unsigned_integer("repositoryFormatVersion", &config::Tree::CORE); @@ -116,6 +120,8 @@ impl Section for Core { &Self::SYMLINKS, &Self::TRUST_C_TIME, &Self::WORKTREE, + &Self::PROTECT_HFS, + &Self::PROTECT_NTFS, &Self::ASKPASS, &Self::EXCLUDES_FILE, &Self::ATTRIBUTES_FILE, diff --git a/gix/src/config/tree/sections/gitoxide.rs b/gix/src/config/tree/sections/gitoxide.rs index a3b05441263..37c706af0e6 100644 --- a/gix/src/config/tree/sections/gitoxide.rs +++ b/gix/src/config/tree/sections/gitoxide.rs @@ -103,6 +103,10 @@ mod subsections { pub const USE_STDEV: keys::Boolean = keys::Boolean::new_boolean("useStdev", &Gitoxide::CORE) .with_note("A runtime version of the USE_STDEV build flag."); + /// The `gitoxide.core.protectWindows` key. + pub const PROTECT_WINDOWS: keys::Boolean = keys::Boolean::new_boolean("protectWindows", &Gitoxide::CORE) + .with_note("enable protections that are enabled by default on Windows"); + /// The `gitoxide.core.shallowFile` key. pub const SHALLOW_FILE: keys::Path = keys::Path::new_path("shallowFile", &Gitoxide::CORE) .with_environment_override("GIT_SHALLOW_FILE") @@ -142,6 +146,7 @@ mod subsections { &Self::USE_NSEC, &Self::USE_STDEV, &Self::SHALLOW_FILE, + &Self::PROTECT_WINDOWS, &Self::FILTER_PROCESS_DELAY, &Self::EXTERNAL_COMMAND_STDERR, &Self::REFS_NAMESPACE, diff --git a/gix/src/filter.rs b/gix/src/filter.rs index c856fe521da..19df012818e 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -135,7 +135,7 @@ impl<'repo> Pipeline<'repo> { impl<'repo> Pipeline<'repo> { /// Convert a `src` stream (to be found at `rela_path`, a repo-relative path) to a representation suitable for storage in `git` /// by using all attributes at `rela_path` and configuration of the repository to know exactly which filters apply. - /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether or not to apply itself, + /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself, /// and it should match the state used when [instantiating this instance][Self::new()]. /// Note that the return-type implements [`std::io::Read`]. pub fn convert_to_git( @@ -147,7 +147,7 @@ impl<'repo> Pipeline<'repo> { where R: std::io::Read, { - let entry = self.cache.at_path(rela_path, Some(false), &self.repo.objects)?; + let entry = self.cache.at_path(rela_path, None, &self.repo.objects)?; Ok(self.inner.convert_to_git( src, rela_path, @@ -179,7 +179,7 @@ impl<'repo> Pipeline<'repo> { can_delay: gix_filter::driver::apply::Delay, ) -> Result, pipeline::convert_to_worktree::Error> { - let entry = self.cache.at_entry(rela_path, Some(false), &self.repo.objects)?; + let entry = self.cache.at_entry(rela_path, None, &self.repo.objects)?; Ok(self.inner.convert_to_worktree( src, rela_path, diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 23f37e9de28..f193ecd74aa 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -345,3 +345,12 @@ pub mod shallow; pub mod discover; pub mod env; + +#[cfg(feature = "attributes")] +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 7c5b065bd78..e5d475c8009 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -216,15 +216,17 @@ impl ThreadSafeRepository { let mut refs = { let reflog = repo_config.reflog.unwrap_or(gix_ref::store::WriteReflog::Disable); let object_hash = repo_config.object_hash; + let ref_store_init_opts = gix_ref::store::init::Options { + write_reflog: reflog, + object_hash, + precompose_unicode: repo_config.precompose_unicode, + prohibit_windows_device_names: repo_config.protect_windows, + }; match &common_dir { - Some(common_dir) => crate::RefStore::for_linked_worktree( - git_dir.to_owned(), - common_dir.into(), - reflog, - object_hash, - repo_config.precompose_unicode, - ), - None => crate::RefStore::at(git_dir.to_owned(), reflog, object_hash, repo_config.precompose_unicode), + Some(common_dir) => { + crate::RefStore::for_linked_worktree(git_dir.to_owned(), common_dir.into(), ref_store_init_opts) + } + None => crate::RefStore::at(git_dir.to_owned(), ref_store_init_opts), } }; let head = refs.find("HEAD").ok(); diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index f501be621f8..4ea3ac7375f 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -137,7 +137,7 @@ impl<'repo> Pathspec<'repo> { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.repo.objects) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.repo.objects) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -193,7 +193,7 @@ impl PathspecDetached { let stack = self.stack.as_mut().expect("initialized in advance"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.odb) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.odb) .map_or(false, |platform| platform.matching_attributes(out)) }, ) @@ -207,3 +207,11 @@ impl PathspecDetached { .map_or(false, |m| !m.is_excluded()) } } + +fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { + if is_dir { + gix_index::entry::Mode::DIR + } else { + gix_index::entry::Mode::FILE + } +} diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index c2600227a33..db64410e42a 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -1,7 +1,7 @@ use crate::bstr::{BStr, BString}; use crate::util::OwnedOrStaticAtomicBool; use crate::worktree::IndexPersistedOrInMemory; -use crate::{config, dirwalk, Repository}; +use crate::{config, dirwalk, is_dir_to_mode, Repository}; use std::sync::atomic::AtomicBool; impl Repository { @@ -64,7 +64,7 @@ impl Repository { .expect("can only be called if attributes are used in patterns"); stack .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.objects) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.objects) .map_or(false, |platform| platform.matching_attributes(out)) }, excludes: Some(&mut excludes.inner), diff --git a/gix/src/repository/filter.rs b/gix/src/repository/filter.rs index d5dc5690ea2..33ee80177bb 100644 --- a/gix/src/repository/filter.rs +++ b/gix/src/repository/filter.rs @@ -12,7 +12,7 @@ pub mod pipeline { #[error(transparent)] DecodeCommit(#[from] gix_object::decode::Error), #[error("Could not create index from tree at HEAD^{{tree}}")] - TreeTraverse(#[from] gix_traverse::tree::breadthfirst::Error), + TreeTraverse(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] BareAttributes(#[from] crate::config::attribute_stack::Error), #[error(transparent)] diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 9d2ba0ccfb1..c50abd3673b 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -111,12 +111,14 @@ impl crate::Repository { /// Create new index-file, which would live at the correct location, in memory from the given `tree`. /// /// Note that this is an expensive operation as it requires recursively traversing the entire tree to unpack it into the index. - pub fn index_from_tree( - &self, - tree: &gix_hash::oid, - ) -> Result { + pub fn index_from_tree(&self, tree: &gix_hash::oid) -> Result { Ok(gix_index::File::from_state( - gix_index::State::from_tree(tree, &self.objects)?, + gix_index::State::from_tree(tree, &self.objects, self.config.protect_options()?).map_err(|err| { + super::index_from_tree::Error::IndexFromTree { + id: tree.into(), + source: err, + } + })?, self.git_dir().join("index"), )) } diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 9c2ffab4274..5f3fbc0b215 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -41,12 +41,15 @@ pub mod attributes; mod cache; mod config; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "blob-diff")] pub mod diff; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "dirwalk")] mod dirwalk; /// +#[allow(clippy::empty_docs)] #[cfg(feature = "attributes")] pub mod filter; mod graph; @@ -73,6 +76,24 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[allow(clippy::empty_docs)] +#[cfg(feature = "index")] +pub mod index_from_tree { + /// The error returned by [Repository::index_from_tree()](crate::Repository::index_from_tree). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not create index from tree at {id}")] + IndexFromTree { + id: gix_hash::ObjectId, + source: gix_index::init::from_tree::Error, + }, + #[error("Couldn't obtain configuration for core.protect*")] + BooleanConfig(#[from] crate::config::boolean::Error), + } +} + /// #[allow(clippy::empty_docs)] pub mod branch_remote_ref_name { @@ -133,7 +154,7 @@ pub mod index_or_load_from_head { #[error(transparent)] TreeId(#[from] gix_object::decode::Error), #[error(transparent)] - TraverseTree(#[from] gix_traverse::tree::breadthfirst::Error), + TraverseTree(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] OpenIndex(#[from] crate::worktree::open_index::Error), } @@ -149,7 +170,7 @@ pub mod worktree_stream { #[error(transparent)] FindTree(#[from] crate::object::find::existing::Error), #[error(transparent)] - OpenTree(#[from] gix_traverse::tree::breadthfirst::Error), + OpenTree(#[from] crate::repository::index_from_tree::Error), #[error(transparent)] AttributesCache(#[from] crate::config::attribute_stack::Error), #[error(transparent)] diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 529243896ea..f24673b296e 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -86,7 +86,7 @@ impl crate::Repository { objects.clone(), pipeline, move |path, mode, attrs| -> std::io::Result<()> { - let entry = cache.at_entry(path, Some(mode.is_tree()), &objects)?; + let entry = cache.at_entry(path, Some(mode.into()), &objects)?; entry.matching_attributes(attrs); Ok(()) }, diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 0e000eb8033..dfeb34ef8cf 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -9,7 +9,7 @@ use std::{ pub use gix_submodule::*; -use crate::{bstr::BStr, worktree::IndexPersistedOrInMemory, Repository, Submodule}; +use crate::{bstr::BStr, is_dir_to_mode, worktree::IndexPersistedOrInMemory, Repository, Submodule}; pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared>; /// A lazily loaded and auto-updated worktree index. @@ -154,7 +154,7 @@ impl<'repo> Submodule<'repo> { &mut |relative_path, case, is_dir, out| { attributes .set_case(case) - .at_entry(relative_path, Some(is_dir), &self.state.repo.objects) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) .map_or(false, |platform| platform.matching_attributes(out)) } })?; diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index 28215273d99..c05b7f7ee11 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -94,14 +94,6 @@ static GIT_CONFIG: &[Record] = &[ config: "core.loosecompression", usage: Planned("") }, - Record { - config: "core.protectHFS", - usage: Planned("relevant for checkout on MacOS") - }, - Record { - config: "core.protectNTFS", - usage: NotPlanned("lack of demand") - }, Record { config: "core.sparseCheckout", usage: Planned("we want to support huge repos and be the fastest in doing so")