mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-02 01:00:09 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a52957926 | |||
| a694b01eb1 |
Generated
+113
-49
@@ -299,7 +299,7 @@ dependencies = [
|
||||
"strum",
|
||||
"thiserror 2.0.21",
|
||||
"uuid",
|
||||
"zstd 0.13.3",
|
||||
"zstd 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -492,7 +492,7 @@ dependencies = [
|
||||
"arrow-select",
|
||||
"flatbuffers",
|
||||
"lz4_flex",
|
||||
"zstd 0.13.3",
|
||||
"zstd 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -960,9 +960,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kms"
|
||||
version = "1.128.0"
|
||||
version = "1.129.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef519fda62fd0fdc066948f5ef33ecb8ae1bf0187bf1a12e8a775c93c4c47e7e"
|
||||
checksum = "5c5cdb29ad25d71c9ee54c408a672c98d90a24a0ea6eb063025483e0e894b411"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -985,9 +985,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.156.0"
|
||||
version = "1.157.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1072c9aa20159b9b417115342d38c4c53108fc98e5359fcb86b7d4575747eb59"
|
||||
checksum = "6ea9794c741520d8baec9462bcd0ad12164d4f5f78d008a8da9d6a0b7222caa8"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -1021,9 +1021,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.118.0"
|
||||
version = "1.119.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4a89786db8ee6dff2c9302a354ef081354fcb0f527e16ee3cb249a6a5cb6df7"
|
||||
checksum = "3c414f25ec4aa9dc18f5cc8f55e18848bb509fcda4ce7c77bee6072e95806bc9"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -1046,9 +1046,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.120.0"
|
||||
version = "1.121.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521c6a11a25f1244efddf947523f277a51a63f0a7ec228f0f62d826e88106366"
|
||||
checksum = "bc638bb4355e6514334dcc3228cd4622f5ee54d704bd6a9010a5a91d7856d9d9"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -1071,9 +1071,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.123.0"
|
||||
version = "1.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8754d38d29448bd1d4aa4a6312ed010dfa296b3436eb2c551cc6e42fc6a66a38"
|
||||
checksum = "3f8a0dd8fbde04ba58ad722113c7addb11bf94cf549b1ea0eb9ea427fb87198a"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -3942,6 +3942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
@@ -5277,9 +5278,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hotpath"
|
||||
version = "0.30.2"
|
||||
version = "0.30.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39e941010d86063ff233bf8325269bf68097fb24ba67000fbddc0a410e8056e"
|
||||
checksum = "d9632c1fd213e63c1b88d8f3e59543da4999b17f859cc1e1d67cf8f2207e8935"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-channel",
|
||||
@@ -5312,15 +5313,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-drain"
|
||||
version = "0.30.2"
|
||||
version = "0.30.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0faae1a9e1fd8ce5debaaaf452dcbe833fc8a8a4f3779d2ec8828b116c3c16a"
|
||||
checksum = "738324f75aec847dea984f6806402f4f1bdfab437b4700fc8c3ebcad2dff6424"
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-macros"
|
||||
version = "0.30.2"
|
||||
version = "0.30.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a24f5179ff680132003306354efd0ac8cde12e87b1a788719c836e91af35cb58"
|
||||
checksum = "5d92fdfec803932e5764ea93c201cf7c6c2d8cff1871da44f64f5094c9c3ef8a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5941,7 +5942,7 @@ dependencies = [
|
||||
"lz4",
|
||||
"snap",
|
||||
"uuid",
|
||||
"zstd 0.13.3",
|
||||
"zstd 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6034,6 +6035,9 @@ name = "lazy_static"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
dependencies = [
|
||||
"spin 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
@@ -6243,9 +6247,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
@@ -6609,9 +6613,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ml-kem"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9ba01ebcf90f6aff8e9b9f55bb69b19e6250e4ade5787d0040eaa27dd226238"
|
||||
checksum = "84ca78b66f46050d49fa21693ae8adab2081d8101b65d8067316b6b70b64a0d1"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
"kem",
|
||||
@@ -6624,9 +6628,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "module-lattice"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
|
||||
checksum = "c4a1e412c8ee83a1c678f427806219c49c96b2511b9f07497514f0e43fb26155"
|
||||
dependencies = [
|
||||
"ctutils",
|
||||
"hybrid-array",
|
||||
@@ -6922,6 +6926,22 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.8",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
@@ -6946,6 +6966,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
@@ -7017,18 +7047,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "5.1.0"
|
||||
source = "git+https://github.com/ramosbugs/oauth2-rs?rev=89585f4a280afd7b285eaf1c9be0068874e03d22#89585f4a280afd7b285eaf1c9be0068874e03d22"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.17",
|
||||
"http 1.5.0",
|
||||
"rand 0.10.3",
|
||||
"rand 0.8.8",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2 0.11.1",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
@@ -7190,29 +7221,31 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "openidconnect"
|
||||
version = "4.0.1"
|
||||
source = "git+https://github.com/ramosbugs/openidconnect-rs?rev=2f77ee73bc4554e1cb6279adb386005429fb4e41#2f77ee73bc4554e1cb6279adb386005429fb4e41"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0d098a59353679624d406825940139e2b2d554a52b64c9fb1b1bc42a11fe5c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ed25519-dalek 3.0.0",
|
||||
"hmac 0.13.0",
|
||||
"getrandom 0.4.3",
|
||||
"hmac 0.12.1",
|
||||
"http 1.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"oauth2",
|
||||
"p256",
|
||||
"p384",
|
||||
"rand 0.10.3",
|
||||
"rsa",
|
||||
"rand 0.8.8",
|
||||
"rsa 0.9.10",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_plain",
|
||||
"serde_with",
|
||||
"sha2 0.11.1",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
@@ -7564,7 +7597,7 @@ dependencies = [
|
||||
"snap",
|
||||
"tokio",
|
||||
"twox-hash",
|
||||
"zstd 0.13.3",
|
||||
"zstd 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7816,6 +7849,17 @@ dependencies = [
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der 0.7.10",
|
||||
"pkcs8 0.10.2",
|
||||
"spki 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.8.0-rc.4"
|
||||
@@ -8914,9 +8958,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reflink-copy"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b"
|
||||
checksum = "3dd31f458a66eeda05a9e837fc5f0effbaacbe8565868ecae5b37bf0544c19f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -9078,6 +9122,26 @@ dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1 0.7.5",
|
||||
"pkcs8 0.10.2",
|
||||
"rand_core 0.6.4",
|
||||
"signature 2.2.0",
|
||||
"spki 0.7.3",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.10.0-rc.18"
|
||||
@@ -9088,7 +9152,7 @@ dependencies = [
|
||||
"crypto-bigint",
|
||||
"crypto-primes",
|
||||
"digest 0.11.3",
|
||||
"pkcs1",
|
||||
"pkcs1 0.8.0-rc.4",
|
||||
"pkcs8 0.11.0",
|
||||
"rand_core 0.10.1",
|
||||
"sha2 0.11.1",
|
||||
@@ -9193,13 +9257,13 @@ dependencies = [
|
||||
"p521",
|
||||
"pageant",
|
||||
"pbkdf2 0.13.0",
|
||||
"pkcs1",
|
||||
"pkcs1 0.8.0-rc.4",
|
||||
"pkcs5 0.8.1",
|
||||
"pkcs8 0.11.0",
|
||||
"polyval",
|
||||
"rand 0.10.3",
|
||||
"rand_core 0.10.1",
|
||||
"rsa",
|
||||
"rsa 0.10.0-rc.18",
|
||||
"russh-cryptovec",
|
||||
"russh-util",
|
||||
"salsa20 0.11.0",
|
||||
@@ -9391,7 +9455,7 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rmp-serde",
|
||||
"rsa",
|
||||
"rsa 0.10.0-rc.18",
|
||||
"rust-embed",
|
||||
"rustfs-audit",
|
||||
"rustfs-common",
|
||||
@@ -9594,7 +9658,7 @@ dependencies = [
|
||||
"jsonwebtoken",
|
||||
"pbkdf2 0.13.0",
|
||||
"rand 0.10.3",
|
||||
"rsa",
|
||||
"rsa 0.10.0-rc.18",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.1",
|
||||
@@ -11988,7 +12052,7 @@ dependencies = [
|
||||
"p384",
|
||||
"p521",
|
||||
"rand_core 0.10.1",
|
||||
"rsa",
|
||||
"rsa 0.10.0-rc.18",
|
||||
"sec1",
|
||||
"serde",
|
||||
"sha1 0.11.0",
|
||||
@@ -13096,9 +13160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
@@ -14000,9 +14064,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.3"
|
||||
version = "9.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
|
||||
checksum = "848dad21639597378c3be94c59fd4de683f0f6d8a5387575896e1245737d4f37"
|
||||
dependencies = [
|
||||
"aes 0.9.3",
|
||||
"bzip2",
|
||||
@@ -14053,7 +14117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
version = "0.13.4"
|
||||
dependencies = [
|
||||
"apache-avro",
|
||||
"arrow-array",
|
||||
|
||||
+6
-10
@@ -217,7 +217,7 @@ chacha20poly1305 = { version = "0.11.0" }
|
||||
crc-fast = "1.10.0"
|
||||
hmac = { version = "0.13.0" }
|
||||
jsonwebtoken = { version = "11.1.0" }
|
||||
openidconnect = { default-features = false, version = "4.0" }
|
||||
openidconnect = { default-features = false, version = "4.1.0" }
|
||||
pbkdf2 = "0.13.0"
|
||||
p256 = { version = "0.14.0", features = ["ecdsa", "pkcs8"] }
|
||||
rsa = { version = "=0.10.0-rc.18" }
|
||||
@@ -253,9 +253,9 @@ atoi = "3.1.1"
|
||||
atomic_enum = "0.3.0"
|
||||
aws-config = { version = "1.13.0" }
|
||||
aws-credential-types = { version = "1.3.1" }
|
||||
aws-sdk-kms = { default-features = false, version = "1.128.0" }
|
||||
aws-sdk-s3 = { default-features = false, version = "1.156.0" }
|
||||
aws-sdk-sts = { default-features = false, version = "1.123.0" }
|
||||
aws-sdk-kms = { default-features = false, version = "1.129.0" }
|
||||
aws-sdk-s3 = { default-features = false, version = "1.157.0" }
|
||||
aws-sdk-sts = { default-features = false, version = "1.124.0" }
|
||||
aws-smithy-async = { version = "1.3.1" }
|
||||
aws-smithy-http-client = { default-features = false, version = "1.5.2" }
|
||||
aws-smithy-runtime-api = { version = "1.19.2" }
|
||||
@@ -356,7 +356,7 @@ winapi-util = "0.1.11"
|
||||
windows = { version = "0.62.2" }
|
||||
windows-sys = "0.61.2"
|
||||
xxhash-rust = { version = "0.8.19" }
|
||||
zip = "9.1.3"
|
||||
zip = "9.2.1"
|
||||
zstd = "0.14.3"
|
||||
|
||||
# Observability and Metrics
|
||||
@@ -386,7 +386,7 @@ dav-server = "0.11.0"
|
||||
# Performance Analysis and Memory Profiling
|
||||
rustfs-mimalloc = { version = "0.7.2" }
|
||||
# Preserve Unicode focus filters until rustfs/backlog#2302 is resolved.
|
||||
hotpath = { version = "0.30.2", default-features = false }
|
||||
hotpath = { version = "0.30.3", default-features = false }
|
||||
# Snapshot testing for output format regression detection
|
||||
insta = { version = "1.49" }
|
||||
|
||||
@@ -427,7 +427,3 @@ strip = "none"
|
||||
# Pin upstream fixes until compatible crates.io releases are available.
|
||||
[patch.crates-io]
|
||||
zstd = { path = "crates/zstd-compat" }
|
||||
# Preserve RustFS OIDC flows while migrating the old elliptic-curve dependency chain.
|
||||
# Upstream: openidconnect-rs#244 and oauth2-rs#352.
|
||||
openidconnect = { git = "https://github.com/ramosbugs/openidconnect-rs", rev = "2f77ee73bc4554e1cb6279adb386005429fb4e41" }
|
||||
oauth2 = { git = "https://github.com/ramosbugs/oauth2-rs", rev = "89585f4a280afd7b285eaf1c9be0068874e03d22" }
|
||||
|
||||
@@ -8033,14 +8033,6 @@ impl LocalDisk {
|
||||
fn io_get_object_open_path(&self, bucket: &str, key: &str) -> Result<PathBuf> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_clean_relative_segment(bucket) && is_clean_relative_segment(key) {
|
||||
let root = self.io_root();
|
||||
let mut path = PathBuf::with_capacity(root.as_os_str().len() + bucket.len() + key.len() + 2);
|
||||
path.push(root);
|
||||
path.push(bucket);
|
||||
path.push(key);
|
||||
return Ok(path);
|
||||
}
|
||||
let (bucket_path, path) = build_local_disk_object_path(self.io_root(), bucket, key);
|
||||
check_local_disk_object_path_components(self.io_root(), &bucket_path, &path)?;
|
||||
Ok(path)
|
||||
@@ -8055,11 +8047,6 @@ impl LocalDisk {
|
||||
fn io_get_bucket_and_object_open_path(&self, bucket: &str, key: &str) -> Result<(PathBuf, PathBuf)> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_clean_relative_segment(bucket) && is_clean_relative_segment(key) {
|
||||
let bucket_path = self.io_root().join(bucket);
|
||||
let path = bucket_path.join(key);
|
||||
return Ok((bucket_path, path));
|
||||
}
|
||||
let (bucket_path, path) = build_local_disk_object_path(self.io_root(), bucket, key);
|
||||
let normalized = check_local_disk_object_path_components(self.io_root(), &bucket_path, &path)?;
|
||||
if matches!(normalized, std::borrow::Cow::Owned(_)) {
|
||||
@@ -12040,7 +12027,8 @@ fn local_disk_object_path(root: &Path, bucket: &str, key: &str) -> Result<PathBu
|
||||
#[cfg(not(windows))]
|
||||
#[inline]
|
||||
fn is_clean_relative_segment(s: &str) -> bool {
|
||||
!s.starts_with('/') && rustfs_utils::path::unix_is_canonical_ascii_path(s.as_bytes())
|
||||
let b = s.as_bytes();
|
||||
!matches!(b.first(), None | Some(b'/' | b'.')) && !b.ends_with(b"/") && !b.windows(2).any(|w| matches!(w, b"//" | b"/."))
|
||||
}
|
||||
|
||||
fn build_local_disk_object_path(root: &Path, bucket: &str, key: &str) -> (PathBuf, PathBuf) {
|
||||
@@ -12087,26 +12075,10 @@ fn build_local_disk_bucket_path(root: &Path, bucket: &str) -> PathBuf {
|
||||
bucket_path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn disk_path_starts_with_normalized(path: &Path, base: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
return rustfs_utils::path::unix_normalized_path_starts_with(path, base);
|
||||
#[cfg(windows)]
|
||||
path.starts_with(base)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn disk_path_strip_prefix_normalized<'a>(path: &'a Path, base: &Path) -> Result<&'a Path> {
|
||||
#[cfg(not(windows))]
|
||||
return rustfs_utils::path::unix_normalized_path_strip_prefix(path, base).ok_or(DiskError::InvalidPath);
|
||||
#[cfg(windows)]
|
||||
path.strip_prefix(base).map_err(|_| DiskError::InvalidPath)
|
||||
}
|
||||
|
||||
fn check_local_disk_valid_object_path(root: &Path, bucket_path: &Path, path: &Path) -> Result<()> {
|
||||
let bucket_path = normalize_path_cow(bucket_path);
|
||||
let path = normalize_path_cow(path);
|
||||
if !disk_path_starts_with_normalized(&bucket_path, root) || !disk_path_starts_with_normalized(&path, &bucket_path) {
|
||||
if !bucket_path.starts_with(root) || !path.starts_with(&bucket_path) {
|
||||
return Err(DiskError::InvalidPath);
|
||||
}
|
||||
|
||||
@@ -12115,7 +12087,7 @@ fn check_local_disk_valid_object_path(root: &Path, bucket_path: &Path, path: &Pa
|
||||
|
||||
fn check_local_disk_valid_path(root: &Path, path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = normalize_path_cow(path.as_ref());
|
||||
if !disk_path_starts_with_normalized(&path, root) {
|
||||
if !path.starts_with(root) {
|
||||
return Err(DiskError::InvalidPath);
|
||||
}
|
||||
|
||||
@@ -12136,7 +12108,7 @@ fn check_local_disk_object_path_components<'a>(
|
||||
) -> Result<std::borrow::Cow<'a, Path>> {
|
||||
let bucket_path = normalize_path_cow(bucket_path);
|
||||
let path = normalize_path_cow(path);
|
||||
if !disk_path_starts_with_normalized(&bucket_path, root) || !disk_path_starts_with_normalized(&path, &bucket_path) {
|
||||
if !bucket_path.starts_with(root) || !path.starts_with(&bucket_path) {
|
||||
return Err(DiskError::InvalidPath);
|
||||
}
|
||||
|
||||
@@ -12146,7 +12118,7 @@ fn check_local_disk_object_path_components<'a>(
|
||||
#[cfg(target_os = "linux")]
|
||||
fn check_local_disk_valid_path_at(root: &Path, root_fd: &std::fs::File, path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = normalize_path_cow(path.as_ref());
|
||||
if !disk_path_starts_with_normalized(&path, root) {
|
||||
if !path.starts_with(root) {
|
||||
return Err(DiskError::InvalidPath);
|
||||
}
|
||||
|
||||
@@ -12155,7 +12127,7 @@ fn check_local_disk_valid_path_at(root: &Path, root_fd: &std::fs::File, path: im
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn reject_local_disk_symlink_components_at(root: &Path, root_fd: &std::fs::File, path: &Path) -> Result<()> {
|
||||
let relative = disk_path_strip_prefix_normalized(path, root)?;
|
||||
let relative = path.strip_prefix(root).map_err(|_| DiskError::InvalidPath)?;
|
||||
match validate_existing_local_disk_prefix_at(root_fd, relative) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(LocalDiskPathValidationAtError::Unsupported) => reject_local_disk_symlink_components(root, path),
|
||||
@@ -12183,11 +12155,11 @@ fn validate_existing_local_disk_prefix_at(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut candidate = relative;
|
||||
let mut candidate = relative.to_path_buf();
|
||||
loop {
|
||||
match openat2(
|
||||
root_fd,
|
||||
candidate,
|
||||
&candidate,
|
||||
OFlags::PATH | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS,
|
||||
@@ -12196,12 +12168,10 @@ fn validate_existing_local_disk_prefix_at(
|
||||
Err(Errno::NOSYS) => return Err(LocalDiskPathValidationAtError::Unsupported),
|
||||
Err(Errno::LOOP | Errno::XDEV) => return Err(LocalDiskPathValidationAtError::InvalidPath),
|
||||
Err(Errno::NOENT) => {
|
||||
let Some(parent) =
|
||||
rustfs_utils::path::unix_path_parent(candidate).filter(|parent| !parent.as_os_str().is_empty())
|
||||
else {
|
||||
let Some(parent) = candidate.parent().filter(|parent| !parent.as_os_str().is_empty()) else {
|
||||
return Ok(());
|
||||
};
|
||||
candidate = parent;
|
||||
candidate = parent.to_path_buf();
|
||||
}
|
||||
Err(err) => return Err(LocalDiskPathValidationAtError::Io(err.into())),
|
||||
}
|
||||
@@ -12209,7 +12179,7 @@ fn validate_existing_local_disk_prefix_at(
|
||||
}
|
||||
|
||||
fn reject_local_disk_symlink_components(root: &Path, path: &Path) -> Result<()> {
|
||||
let relative = disk_path_strip_prefix_normalized(path, root)?;
|
||||
let relative = path.strip_prefix(root).map_err(|_| DiskError::InvalidPath)?;
|
||||
let mut current = root.to_path_buf();
|
||||
|
||||
for component in relative.components() {
|
||||
@@ -12233,11 +12203,12 @@ fn reject_local_disk_symlink_components(root: &Path, path: &Path) -> Result<()>
|
||||
fn normalize_path_cow(path: &Path) -> std::borrow::Cow<'_, Path> {
|
||||
// Windows prefixes retain the existing PathBuf reconstruction semantics.
|
||||
#[cfg(not(windows))]
|
||||
if !path.as_os_str().as_encoded_bytes().contains(&b'.')
|
||||
|| !path
|
||||
.components()
|
||||
.any(|c| matches!(c, std::path::Component::ParentDir | std::path::Component::CurDir))
|
||||
{
|
||||
let bytes = path.as_os_str().as_encoded_bytes();
|
||||
if bytes != b"." && !bytes.starts_with(b"./") && !rustfs_utils::path::unix_path_has_dotdot_segment(bytes) {
|
||||
return std::borrow::Cow::Borrowed(path);
|
||||
}
|
||||
return std::borrow::Cow::Borrowed(path);
|
||||
}
|
||||
std::borrow::Cow::Owned(normalize_path_components(path))
|
||||
}
|
||||
|
||||
@@ -4439,18 +4439,12 @@ fn prepare_destination_parent_with_retry(
|
||||
base_dir: &Path,
|
||||
publication_root: &PublicationRoot,
|
||||
) -> io::Result<ExistingBaseDirectoryGuard> {
|
||||
let mut prune_budget = None;
|
||||
let prune_budget = parent_prune_budget(parent, base_dir);
|
||||
let mut attempt = 0;
|
||||
loop {
|
||||
match mkdir_all_below_existing_base_std(parent, base_dir, publication_root) {
|
||||
Ok(preparation) => return Ok(preparation),
|
||||
Err(err)
|
||||
if should_retry_prepare(
|
||||
&err,
|
||||
attempt,
|
||||
*prune_budget.get_or_insert_with(|| parent_prune_budget(parent, base_dir)),
|
||||
) =>
|
||||
{
|
||||
Err(err) if should_retry_prepare(&err, attempt, prune_budget) => {
|
||||
attempt += 1;
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
@@ -4942,9 +4936,13 @@ impl PublicationRoot {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use rustix::fs::{Mode, OFlags};
|
||||
let (parent_path, name) = rustfs_utils::path::unix_split_parent_and_file_name(path)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing file parent or name"))?;
|
||||
let parent = self.open_directory(parent_path)?;
|
||||
let parent = self.open_directory(
|
||||
path.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing file parent"))?,
|
||||
)?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing file name"))?;
|
||||
let file = open_linux_relative(
|
||||
&parent,
|
||||
Path::new(name),
|
||||
@@ -4984,9 +4982,9 @@ impl PublicationRoot {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn relative_path<'a>(&self, path: &'a Path) -> io::Result<&'a Path> {
|
||||
rustfs_utils::path::unix_normalized_path_strip_prefix(path, self.io_path.as_path())
|
||||
.or_else(|| rustfs_utils::path::unix_normalized_path_strip_prefix(path, self.path.as_path()))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path must remain below its publication root"))
|
||||
path.strip_prefix(self.io_path.as_path())
|
||||
.or_else(|_| path.strip_prefix(self.path.as_path()))
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path must remain below its publication root"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -4998,13 +4996,6 @@ impl PublicationRoot {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn linux_relative_path_has_invalid_component(relative: &Path) -> bool {
|
||||
let bytes = relative.as_os_str().as_encoded_bytes();
|
||||
bytes.starts_with(b"/") || rustfs_utils::path::unix_path_has_dotdot_segment(bytes)
|
||||
}
|
||||
|
||||
/// Open the actual IO handle with the same containment policy as path validation.
|
||||
/// Older kernels retain fd-relative traversal; only ENOSYS enables the fallback.
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -5015,7 +5006,10 @@ fn open_linux_relative(
|
||||
mode: rustix::fs::Mode,
|
||||
) -> io::Result<std::fs::File> {
|
||||
use rustix::fs::{OFlags, ResolveFlags, openat2};
|
||||
if linux_relative_path_has_invalid_component(relative) {
|
||||
if relative
|
||||
.components()
|
||||
.any(|component| !matches!(component, Component::Normal(_) | Component::CurDir))
|
||||
{
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid relative disk path"));
|
||||
}
|
||||
let relative = if relative.as_os_str().is_empty() {
|
||||
@@ -5186,8 +5180,11 @@ impl LinuxCommitDirectories {
|
||||
|
||||
fn file_name<'a>(&self, path: &'a Path, source: bool) -> io::Result<&'a std::ffi::OsStr> {
|
||||
let parent = if source { &self.source_path } else { &self.destination_path };
|
||||
rustfs_utils::path::unix_direct_child_file_name(path, parent)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file parent does not match its commit guard"))
|
||||
if path.parent() != Some(parent) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "file parent does not match its commit guard"));
|
||||
}
|
||||
path.file_name()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing file name"))
|
||||
}
|
||||
|
||||
fn validate(&self) -> io::Result<()> {
|
||||
@@ -5199,8 +5196,8 @@ impl LinuxCommitDirectories {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "commit directory identity changed"));
|
||||
}
|
||||
let destination = self.prepare_destination()?;
|
||||
if (self.source.device, self.source.inode) != (destination.device, destination.inode)
|
||||
|| self.source_path.as_os_str() != self.destination_path.as_os_str()
|
||||
if self.source_path != self.destination_path
|
||||
|| (self.source.device, self.source.inode) != (destination.device, destination.inode)
|
||||
{
|
||||
let current = self.root.open_directory(&self.destination_path)?.metadata()?;
|
||||
if (current.dev(), current.ino()) != (destination.device, destination.inode) {
|
||||
@@ -5219,10 +5216,7 @@ impl LinuxCommitDirectories {
|
||||
loop {
|
||||
if let Some(prepared) = prepared {
|
||||
let metadata = rustix::fs::statat(&*self.source.file, source_name, rustix::fs::AtFlags::SYMLINK_NOFOLLOW)?;
|
||||
if (prepared.path.as_os_str() != source.as_os_str() && prepared.path != source)
|
||||
|| metadata.st_dev != prepared.device
|
||||
|| metadata.st_ino != prepared.inode
|
||||
{
|
||||
if prepared.path != source || metadata.st_dev != prepared.device || metadata.st_ino != prepared.inode {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"rename source identity changed while publication was prepared",
|
||||
@@ -6360,35 +6354,19 @@ fn windows_rename_source_is_allowed(attributes: u32, reparse_tag: u32) -> bool {
|
||||
}
|
||||
|
||||
fn existing_base_directory_suffix<'a>(dir_path: &'a Path, base_dir: &Path) -> io::Result<&'a Path> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let relative = rustfs_utils::path::unix_normalized_path_strip_prefix(dir_path, base_dir).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "rename destination must remain below its base directory")
|
||||
})?;
|
||||
if linux_relative_path_has_invalid_component(relative) {
|
||||
let relative = dir_path
|
||||
.strip_prefix(base_dir)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename destination must remain below its base directory"))?;
|
||||
for component in relative.components() {
|
||||
if !matches!(component, Component::Normal(_) | Component::CurDir) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"rename destination contains an invalid path component",
|
||||
));
|
||||
}
|
||||
Ok(relative)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let relative = dir_path.strip_prefix(base_dir).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "rename destination must remain below its base directory")
|
||||
})?;
|
||||
for component in relative.components() {
|
||||
if !matches!(component, Component::Normal(_) | Component::CurDir) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"rename destination contains an invalid path component",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relative)
|
||||
}
|
||||
Ok(relative)
|
||||
}
|
||||
|
||||
/// Missing state never downgrades surviving native control records to legacy.
|
||||
|
||||
@@ -429,7 +429,6 @@ impl WritePlan {
|
||||
let checksum = reader.content_hash().clone();
|
||||
let trailer = reader.get_trailer().cloned();
|
||||
|
||||
let delegate_inner_etag = self.compression.is_none() && reader.is_etag_reader();
|
||||
let encrypted = self.encryption.is_some();
|
||||
if let Some(algorithm) = self.compression {
|
||||
reader = HashReader::from_reader(
|
||||
@@ -449,8 +448,7 @@ impl WritePlan {
|
||||
}
|
||||
WriteEncryptionMode::SinglepartObjectKey | WriteEncryptionMode::MultipartObjectKey { .. } => [0u8; 12],
|
||||
};
|
||||
let protected_reader = rustfs_rio::EtagReader::new_encrypted(reader, encryption.key_bytes, etag_nonce)?
|
||||
.with_inner_etag_delegation(delegate_inner_etag);
|
||||
let protected_reader = rustfs_rio::EtagReader::new_encrypted(reader, encryption.key_bytes, etag_nonce)?;
|
||||
reader = match encryption.mode {
|
||||
WriteEncryptionMode::SinglepartObjectKey => HashReader::from_reader(
|
||||
#[cfg(feature = "rio-v2")]
|
||||
|
||||
@@ -2432,7 +2432,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
let multipart_part_size = data.size();
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, true)?,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let write_path = classify_multipart_part_write_path(multipart_part_size, fi.erasure.block_size);
|
||||
|
||||
@@ -4534,7 +4534,7 @@ impl SetDisks {
|
||||
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, true)?,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let small_size_hint = if matches!(write_path, SmallWritePath::Inline | SmallWritePath::SingleBlockNonInline) {
|
||||
|
||||
@@ -16,9 +16,10 @@ use super::{
|
||||
transport::{DiscoveryRequest, OidcHttpError, ReqwestHttpClient, oidc_extra_root_certs, oidc_http_error_diagnostics},
|
||||
};
|
||||
use crate::{EVENT_OIDC_DIAGNOSTICS, LOG_COMPONENT_IAM, LOG_SUBSYSTEM_OIDC};
|
||||
use openidconnect::core::{CoreIdTokenVerifier, CoreJsonWebKeySet, CoreJwsSigningAlgorithm};
|
||||
use openidconnect::core::{CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKeySet, CoreJwsSigningAlgorithm};
|
||||
use openidconnect::{
|
||||
AsyncHttpClient, ClientId, ClientSecret, DiscoveryError, IssuerUrl, JsonWebKeySetUrl, ProviderMetadataWithLogout, TokenUrl,
|
||||
AsyncHttpClient, ClaimsVerificationError, ClientId, ClientSecret, DiscoveryError, IssuerUrl, JsonWebKeySetUrl, NonceVerifier,
|
||||
ProviderMetadataWithLogout, SignatureVerificationError, TokenUrl,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -212,6 +213,35 @@ impl DiscoveredProviderMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn verify_id_token_claims<'a, N: NonceVerifier>(
|
||||
token: &'a CoreIdToken,
|
||||
verifier: &CoreIdTokenVerifier<'_>,
|
||||
nonce_verifier: N,
|
||||
) -> Result<&'a CoreIdTokenClaims, ClaimsVerificationError> {
|
||||
let expected_length = match token.signing_alg().map_err(ClaimsVerificationError::SignatureVerification)? {
|
||||
CoreJwsSigningAlgorithm::HmacSha256 => 32,
|
||||
CoreJwsSigningAlgorithm::HmacSha384 => 48,
|
||||
CoreJwsSigningAlgorithm::HmacSha512 => 64,
|
||||
_ => return token.claims(verifier, nonce_verifier),
|
||||
};
|
||||
// RUSTFS_COMPAT_TODO(openidconnect-hmac-signature-width): Reject widths that upstream converts to fixed-size arrays. Remove after upstream returns verification errors for arbitrary HMAC signature lengths.
|
||||
// openidconnect 4.1.0 converts HMAC signatures to fixed-size arrays before verifying them.
|
||||
let invalid_length = || {
|
||||
ClaimsVerificationError::SignatureVerification(SignatureVerificationError::CryptoError(
|
||||
"invalid HMAC signature length".to_owned(),
|
||||
))
|
||||
};
|
||||
let compact = token.to_string();
|
||||
let (_, signature) = compact.rsplit_once('.').ok_or_else(invalid_length)?;
|
||||
let signature = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(signature)
|
||||
.map_err(|_| invalid_length())?;
|
||||
if signature.len() != expected_length {
|
||||
return Err(invalid_length());
|
||||
}
|
||||
token.claims(verifier, nonce_verifier)
|
||||
}
|
||||
|
||||
// This adapter is used only for discovery/JWKS fetches, never token exchange.
|
||||
struct JwksAcceptClient<'a> {
|
||||
inner: &'a ReqwestHttpClient,
|
||||
|
||||
@@ -242,7 +242,7 @@ fn configured_verifier_rejects_malformed_hmac_signature_lengths() {
|
||||
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
||||
let payload = serde_json::json!({
|
||||
"iss": config.config_url, "sub": "synthetic-subject", "aud": config.client_id,
|
||||
"iat": now, "exp": now + 300,
|
||||
"iat": now, "exp": now + 300, "nonce": "synthetic-nonce",
|
||||
});
|
||||
|
||||
for (algorithm, name, signature_length) in [
|
||||
@@ -275,22 +275,28 @@ fn configured_verifier_rejects_malformed_hmac_signature_lengths() {
|
||||
.expect("parse configured provider metadata");
|
||||
let verifier = metadata.verifier(&config);
|
||||
assert!(
|
||||
valid.claims(&verifier, |_: Option<&Nonce>| Ok(())).is_ok(),
|
||||
verify_id_token_claims(&valid, &verifier, |_: Option<&Nonce>| Ok(())).is_ok(),
|
||||
"valid {name} token must pass for workload={workload}"
|
||||
);
|
||||
assert!(verify_id_token_claims(&valid, &verifier, &Nonce::new("synthetic-nonce".into())).is_ok());
|
||||
assert!(verify_id_token_claims(&valid, &verifier, &Nonce::new("wrong-nonce".into())).is_err());
|
||||
|
||||
for length in [1, 0, signature_length - 1, signature_length, signature_length + 1, 128] {
|
||||
let signature = base64_simd::URL_SAFE_NO_PAD.encode_to_string(vec![0_u8; length]);
|
||||
let malformed: CoreIdToken = format!("{signing_input}.{signature}")
|
||||
.parse()
|
||||
.expect("invalid signature length must still reach cryptographic verification");
|
||||
.expect("invalid signature length must remain parseable before verification");
|
||||
assert!(
|
||||
malformed.claims(&verifier, |_: Option<&Nonce>| Ok(())).is_err(),
|
||||
verify_id_token_claims(&malformed, &verifier, |_: Option<&Nonce>| Ok(())).is_err(),
|
||||
"{name} signature length {length} must return an error for workload={workload}"
|
||||
);
|
||||
assert!(
|
||||
verify_id_token_claims(&malformed, &verifier, &Nonce::new("synthetic-nonce".into())).is_err(),
|
||||
"{name} signature length {length} must return an error with a nonce for workload={workload}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
valid.claims(&verifier, |_: Option<&Nonce>| Ok(())).is_ok(),
|
||||
verify_id_token_claims(&valid, &verifier, |_: Option<&Nonce>| Ok(())).is_ok(),
|
||||
"invalid inputs must not prevent later valid {name} verification"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::{
|
||||
OidcConfigQuery, OidcConfigSnapshot, OidcProviderConfig, OidcProviderSummary, OidcSiteReplicationSnapshot,
|
||||
SourcedOidcProviderConfig, load_oidc_config_snapshot,
|
||||
},
|
||||
provider::{ProviderRuntime, discover_provider_for_recovery},
|
||||
provider::{ProviderRuntime, discover_provider_for_recovery, verify_id_token_claims},
|
||||
state::{OidcAuthSession, OidcLogoutSession, OidcStateStore},
|
||||
transport::{OidcExtraRootCaProvider, ReqwestHttpClient, inspect_token_response_body, oidc_http_error_diagnostics},
|
||||
};
|
||||
@@ -535,7 +535,7 @@ impl OidcSys {
|
||||
let verifier = client
|
||||
.id_token_verifier()
|
||||
.set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud));
|
||||
let verified = id_token.claims(&verifier, &Nonce::new(session.nonce.clone()));
|
||||
let verified = verify_id_token_claims(id_token, &verifier, &Nonce::new(session.nonce.clone()));
|
||||
if let Err(e) = verified {
|
||||
let verification_error = e.to_string();
|
||||
warn!(
|
||||
@@ -583,8 +583,7 @@ impl OidcSys {
|
||||
let verifier = client
|
||||
.id_token_verifier()
|
||||
.set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud));
|
||||
id_token
|
||||
.claims(&verifier, &Nonce::new(session.nonce.clone()))
|
||||
verify_id_token_claims(id_token, &verifier, &Nonce::new(session.nonce.clone()))
|
||||
.map_err(|retry_err| {
|
||||
error!(
|
||||
event = EVENT_OIDC_DIAGNOSTICS,
|
||||
@@ -767,7 +766,7 @@ impl OidcSys {
|
||||
.metadata
|
||||
.verifier(&config)
|
||||
.set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud));
|
||||
if let Err(e) = id_token.claims(&verifier, |_: Option<&Nonce>| Ok(())) {
|
||||
if let Err(e) = verify_id_token_claims(&id_token, &verifier, |_: Option<&Nonce>| Ok(())) {
|
||||
state = self
|
||||
.provider_runtime
|
||||
.refresh_provider_state(&provider_id, &config)
|
||||
@@ -780,8 +779,7 @@ impl OidcSys {
|
||||
.metadata
|
||||
.verifier(&config)
|
||||
.set_other_audience_verifier_fn(|aud| trusted_aud(&config.other_audiences, aud));
|
||||
id_token
|
||||
.claims(&verifier, |_: Option<&Nonce>| Ok(()))
|
||||
verify_id_token_claims(&id_token, &verifier, |_: Option<&Nonce>| Ok(()))
|
||||
.map_err(|retry_err| format!("ID token verification failed after JWKS refresh: {retry_err}"))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1081,7 +1081,7 @@ impl ChecksumHasher for Sha512Hasher {
|
||||
/// MD5 hasher for the ADDITIONAL checksum (x-amz-checksum-md5). Separate from the
|
||||
/// legacy Content-MD5 / ETag machinery — this only serves the flexible-checksum path.
|
||||
pub struct Md5Hasher {
|
||||
hasher: rustfs_utils::FastMd5,
|
||||
hasher: md5::Md5,
|
||||
}
|
||||
|
||||
impl Default for Md5Hasher {
|
||||
@@ -1092,14 +1092,14 @@ impl Default for Md5Hasher {
|
||||
|
||||
impl Md5Hasher {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hasher: rustfs_utils::FastMd5::new(),
|
||||
}
|
||||
use md5::Digest as _;
|
||||
Self { hasher: md5::Md5::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Md5Hasher {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
use md5::Digest as _;
|
||||
self.hasher.update(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
@@ -1111,11 +1111,13 @@ impl Write for Md5Hasher {
|
||||
|
||||
impl ChecksumHasher for Md5Hasher {
|
||||
fn finalize(&mut self) -> Vec<u8> {
|
||||
self.hasher.finalize_bytes().to_vec()
|
||||
use md5::Digest as _;
|
||||
self.hasher.clone().finalize().to_vec()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.hasher.reset();
|
||||
use md5::Digest as _;
|
||||
self.hasher = md5::Md5::new();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-102
@@ -15,8 +15,8 @@
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::{BadDigest, EtagResolvable, HashReaderDetector, HashReaderMut};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use md5::{Digest, Md5};
|
||||
use pin_project_lite::pin_project;
|
||||
use rustfs_utils::FastMd5;
|
||||
use sha2::Sha256;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
@@ -27,12 +27,11 @@ pin_project! {
|
||||
pub struct EtagReader<R> {
|
||||
#[pin]
|
||||
pub inner: R,
|
||||
pub md5: FastMd5,
|
||||
pub md5: Md5,
|
||||
pub finished: bool,
|
||||
pub checksum: Option<String>,
|
||||
resolved_etag: Option<String>,
|
||||
encrypted_etag_mac: Option<Hmac<Sha256>>,
|
||||
delegate_inner_etag: bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +39,11 @@ impl<R> EtagReader<R> {
|
||||
pub fn new(inner: R, checksum: Option<String>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
md5: FastMd5::new(),
|
||||
md5: Md5::new(),
|
||||
finished: false,
|
||||
checksum,
|
||||
resolved_etag: None,
|
||||
encrypted_etag_mac: None,
|
||||
delegate_inner_etag: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,17 +58,6 @@ impl<R> EtagReader<R> {
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Reuse the inner reader's plaintext ETag when deriving an encrypted ETag,
|
||||
/// avoiding a redundant second MD5 pass over the same plaintext stream.
|
||||
pub fn with_inner_etag_delegation(mut self, delegate: bool) -> Self
|
||||
where
|
||||
R: EtagResolvable,
|
||||
{
|
||||
self.delegate_inner_etag =
|
||||
delegate && self.encrypted_etag_mac.is_some() && self.checksum.is_none() && self.inner.is_etag_reader();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the final md5 value (etag) as a hex string, only compute once.
|
||||
/// Can be called multiple times, always returns the same result after finished.
|
||||
pub fn get_etag(&mut self) -> String {
|
||||
@@ -78,8 +65,8 @@ impl<R> EtagReader<R> {
|
||||
return etag.clone();
|
||||
}
|
||||
|
||||
let digest = self.md5.finalize_bytes();
|
||||
let etag = hex_simd::encode_to_string(digest, hex_simd::AsciiCase::Lower);
|
||||
let etag = self.md5.clone().finalize().to_vec();
|
||||
let etag = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
self.resolved_etag = Some(etag.clone());
|
||||
etag
|
||||
}
|
||||
@@ -100,32 +87,30 @@ where
|
||||
if let Poll::Ready(Ok(())) = &poll {
|
||||
let filled = &buf.filled()[orig_filled..];
|
||||
if !filled.is_empty() {
|
||||
if !*this.delegate_inner_etag {
|
||||
this.md5.update(filled);
|
||||
}
|
||||
this.md5.update(filled);
|
||||
} else {
|
||||
// EOF
|
||||
*this.finished = true;
|
||||
if let Some(checksum) = this.checksum {
|
||||
let etag = if let Some(etag) = this.resolved_etag.as_ref() {
|
||||
etag.clone()
|
||||
} else {
|
||||
let digest = this.md5.finalize_bytes();
|
||||
let etag = hex_simd::encode_to_string(digest, hex_simd::AsciiCase::Lower);
|
||||
*this.resolved_etag = Some(etag.clone());
|
||||
etag
|
||||
};
|
||||
let etag = if let Some(etag) = this.resolved_etag.as_ref() {
|
||||
etag.clone()
|
||||
} else {
|
||||
let etag = this.md5.clone().finalize().to_vec();
|
||||
let etag = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
*this.resolved_etag = Some(etag.clone());
|
||||
etag
|
||||
};
|
||||
|
||||
if *checksum != etag {
|
||||
error!("Checksum mismatch, expected={:?}, actual={:?}", checksum, etag);
|
||||
return Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
BadDigest {
|
||||
expected_md5: checksum.clone(),
|
||||
calculated_md5: etag,
|
||||
},
|
||||
)));
|
||||
}
|
||||
if let Some(checksum) = this.checksum
|
||||
&& *checksum != etag
|
||||
{
|
||||
error!("Checksum mismatch, expected={:?}, actual={:?}", checksum, etag);
|
||||
return Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
BadDigest {
|
||||
expected_md5: checksum.clone(),
|
||||
calculated_md5: etag,
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,24 +118,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> EtagResolvable for EtagReader<R>
|
||||
where
|
||||
R: EtagResolvable,
|
||||
{
|
||||
impl<R> EtagResolvable for EtagReader<R> {
|
||||
fn is_etag_reader(&self) -> bool {
|
||||
!self.delegate_inner_etag || self.inner.is_etag_reader()
|
||||
true
|
||||
}
|
||||
fn try_resolve_etag(&mut self) -> Option<String> {
|
||||
if let Some(mut mac) = self.encrypted_etag_mac.clone() {
|
||||
if !self.finished {
|
||||
return None;
|
||||
}
|
||||
let plaintext_etag = if self.delegate_inner_etag {
|
||||
self.inner.try_resolve_etag()?
|
||||
} else {
|
||||
self.get_etag()
|
||||
};
|
||||
mac.update(plaintext_etag.as_bytes());
|
||||
mac.update(self.get_etag().as_bytes());
|
||||
let protected = mac.finalize().into_bytes();
|
||||
return Some(hex_simd::encode_to_string(&protected[..16], hex_simd::AsciiCase::Lower));
|
||||
}
|
||||
@@ -190,7 +167,6 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use md5::{Digest, Md5};
|
||||
use rand::RngExt;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
@@ -364,54 +340,4 @@ mod tests {
|
||||
assert_eq!(digest.expected_md5, wrong_checksum);
|
||||
assert_eq!(digest.calculated_md5, calculated_md5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn encrypted_etag_reader_with_inner_delegation_matches_standalone() {
|
||||
let data = b"delegated encrypted etag payload across multiple 64-byte MD5 blocks for verification";
|
||||
let key = [0x42; 32];
|
||||
let nonce = [0x24; 12];
|
||||
|
||||
let mut standalone =
|
||||
EtagReader::new_encrypted(Cursor::new(data.as_slice()), key, nonce).expect("standalone encrypted EtagReader");
|
||||
let mut out_standalone = Vec::new();
|
||||
standalone.read_to_end(&mut out_standalone).await.expect("read standalone");
|
||||
let expected_tag = standalone.try_resolve_etag().expect("standalone etag");
|
||||
|
||||
let inner =
|
||||
crate::HashReader::from_stream(Cursor::new(data.as_slice()), data.len() as i64, data.len() as i64, None, None, false)
|
||||
.expect("inner HashReader");
|
||||
assert!(inner.is_etag_reader(), "HashReader with diskable_md5=false must report is_etag_reader");
|
||||
let mut delegated = EtagReader::new_encrypted(inner, key, nonce)
|
||||
.expect("delegated encrypted EtagReader")
|
||||
.with_inner_etag_delegation(true);
|
||||
let mut out_delegated = Vec::new();
|
||||
delegated.read_to_end(&mut out_delegated).await.expect("read delegated");
|
||||
assert_eq!(out_delegated, data);
|
||||
assert_eq!(
|
||||
delegated.try_resolve_etag().as_deref(),
|
||||
Some(expected_tag.as_str()),
|
||||
"delegated encrypted EtagReader must produce identical HMAC ETag without re-hashing plaintext"
|
||||
);
|
||||
|
||||
let truncated = crate::LimitReader::new(
|
||||
crate::HashReader::from_stream(Cursor::new(data.as_slice()), data.len() as i64, data.len() as i64, None, None, false)
|
||||
.expect("inner HashReader for limit"),
|
||||
16,
|
||||
);
|
||||
assert!(
|
||||
!truncated.is_etag_reader(),
|
||||
"truncating LimitReader must not advertise inner ETag delegation"
|
||||
);
|
||||
|
||||
let mut non_etag_inner = EtagReader::new_encrypted(Cursor::new(data.as_slice()), key, nonce)
|
||||
.expect("encrypted EtagReader over Cursor")
|
||||
.with_inner_etag_delegation(true);
|
||||
let mut out_cursor = Vec::new();
|
||||
non_etag_inner.read_to_end(&mut out_cursor).await.expect("read cursor");
|
||||
assert_eq!(
|
||||
non_etag_inner.try_resolve_etag().as_deref(),
|
||||
Some(expected_tag.as_str()),
|
||||
"delegation request on non-EtagResolvable inner must keep outer MD5 hashing active"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,10 +630,6 @@ impl AsyncRead for HashReader {
|
||||
}
|
||||
|
||||
impl EtagResolvable for HashReader {
|
||||
fn is_etag_reader(&self) -> bool {
|
||||
!self.diskable_md5 && self.inner.is_etag_reader()
|
||||
}
|
||||
|
||||
fn try_resolve_etag(&mut self) -> Option<String> {
|
||||
if self.diskable_md5 {
|
||||
return None;
|
||||
|
||||
@@ -201,31 +201,10 @@ where
|
||||
boxed_reader(WarpReader::new(reader))
|
||||
}
|
||||
|
||||
impl<T> EtagResolvable for std::io::Cursor<T> {}
|
||||
|
||||
impl EtagResolvable for &[u8] {}
|
||||
|
||||
impl<R> EtagResolvable for tokio::io::BufReader<R>
|
||||
where
|
||||
R: tokio::io::AsyncRead + EtagResolvable,
|
||||
{
|
||||
fn is_etag_reader(&self) -> bool {
|
||||
self.get_ref().is_etag_reader()
|
||||
}
|
||||
|
||||
fn try_resolve_etag(&mut self) -> Option<String> {
|
||||
self.get_mut().try_resolve_etag()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EtagResolvable for Box<T>
|
||||
where
|
||||
T: EtagResolvable + ?Sized,
|
||||
{
|
||||
fn is_etag_reader(&self) -> bool {
|
||||
self.as_ref().is_etag_reader()
|
||||
}
|
||||
|
||||
fn try_resolve_etag(&mut self) -> Option<String> {
|
||||
self.as_mut().try_resolve_etag()
|
||||
}
|
||||
|
||||
+4
-315
@@ -14,300 +14,9 @@
|
||||
|
||||
use blake2::Blake2b512;
|
||||
use highway::{HighwayHash, HighwayHasher, Key};
|
||||
use md5::Md5;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::hint::black_box;
|
||||
|
||||
const MD5_INITIAL_STATE: [u32; 4] = [0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476];
|
||||
|
||||
const MD5_RC: [u32; 64] = [
|
||||
0xd76a_a478,
|
||||
0xe8c7_b756,
|
||||
0x2420_70db,
|
||||
0xc1bd_ceee,
|
||||
0xf57c_0faf,
|
||||
0x4787_c62a,
|
||||
0xa830_4613,
|
||||
0xfd46_9501,
|
||||
0x6980_98d8,
|
||||
0x8b44_f7af,
|
||||
0xffff_5bb1,
|
||||
0x895c_d7be,
|
||||
0x6b90_1122,
|
||||
0xfd98_7193,
|
||||
0xa679_438e,
|
||||
0x49b4_0821,
|
||||
0xf61e_2562,
|
||||
0xc040_b340,
|
||||
0x265e_5a51,
|
||||
0xe9b6_c7aa,
|
||||
0xd62f_105d,
|
||||
0x0244_1453,
|
||||
0xd8a1_e681,
|
||||
0xe7d3_fbc8,
|
||||
0x21e1_cde6,
|
||||
0xc337_07d6,
|
||||
0xf4d5_0d87,
|
||||
0x455a_14ed,
|
||||
0xa9e3_e905,
|
||||
0xfcef_a3f8,
|
||||
0x676f_02d9,
|
||||
0x8d2a_4c8a,
|
||||
0xfffa_3942,
|
||||
0x8771_f681,
|
||||
0x6d9d_6122,
|
||||
0xfde5_380c,
|
||||
0xa4be_ea44,
|
||||
0x4bde_cfa9,
|
||||
0xf6bb_4b60,
|
||||
0xbebf_bc70,
|
||||
0x289b_7ec6,
|
||||
0xeaa1_27fa,
|
||||
0xd4ef_3085,
|
||||
0x0488_1d05,
|
||||
0xd9d4_d039,
|
||||
0xe6db_99e5,
|
||||
0x1fa2_7cf8,
|
||||
0xc4ac_5665,
|
||||
0xf429_2244,
|
||||
0x432a_ff97,
|
||||
0xab94_23a7,
|
||||
0xfc93_a039,
|
||||
0x655b_59c3,
|
||||
0x8f0c_cc92,
|
||||
0xffef_f47d,
|
||||
0x8584_5dd1,
|
||||
0x6fa8_7e4f,
|
||||
0xfe2c_e6e0,
|
||||
0xa301_4314,
|
||||
0x4e08_11a1,
|
||||
0xf753_7e82,
|
||||
0xbd3a_f235,
|
||||
0x2ad7_d2bb,
|
||||
0xeb86_d391,
|
||||
];
|
||||
|
||||
#[inline(always)]
|
||||
fn md5_op_f(w: u32, x: u32, y: u32, z: u32, m: u32, c: u32, s: u32) -> u32 {
|
||||
// Keep `w + m + c` off the `x` critical dependency path from the previous round.
|
||||
let base = black_box(w.wrapping_add(m).wrapping_add(c));
|
||||
let f = z ^ (x & (y ^ z));
|
||||
base.wrapping_add(f).rotate_left(s).wrapping_add(x)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn md5_op_g(w: u32, x: u32, y: u32, z: u32, m: u32, c: u32, s: u32) -> u32 {
|
||||
let base = black_box(w.wrapping_add(m).wrapping_add(c)).wrapping_add(y & !z);
|
||||
base.wrapping_add(x & z).rotate_left(s).wrapping_add(x)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn md5_op_h(w: u32, x: u32, y: u32, z: u32, m: u32, c: u32, s: u32) -> u32 {
|
||||
let base = black_box(w.wrapping_add(m).wrapping_add(c));
|
||||
base.wrapping_add(x ^ (y ^ z)).rotate_left(s).wrapping_add(x)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn md5_op_i(w: u32, x: u32, y: u32, z: u32, m: u32, c: u32, s: u32) -> u32 {
|
||||
let base = black_box(w.wrapping_add(m).wrapping_add(c));
|
||||
base.wrapping_add(y ^ (x | !z)).rotate_left(s).wrapping_add(x)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn md5_word(words: &[[u8; 4]], idx: usize) -> u32 {
|
||||
u32::from_le_bytes(words[idx])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn compress_md5_blocks(state: &mut [u32; 4], blocks: &[[u8; 64]]) {
|
||||
let [mut a, mut b, mut c, mut d] = *state;
|
||||
for block in blocks {
|
||||
let words = block.as_chunks::<4>().0;
|
||||
let aa = a;
|
||||
let bb = b;
|
||||
let cc = c;
|
||||
let dd = d;
|
||||
|
||||
a = md5_op_f(a, b, c, d, md5_word(words, 0), MD5_RC[0], 7);
|
||||
d = md5_op_f(d, a, b, c, md5_word(words, 1), MD5_RC[1], 12);
|
||||
c = md5_op_f(c, d, a, b, md5_word(words, 2), MD5_RC[2], 17);
|
||||
b = md5_op_f(b, c, d, a, md5_word(words, 3), MD5_RC[3], 22);
|
||||
a = md5_op_f(a, b, c, d, md5_word(words, 4), MD5_RC[4], 7);
|
||||
d = md5_op_f(d, a, b, c, md5_word(words, 5), MD5_RC[5], 12);
|
||||
c = md5_op_f(c, d, a, b, md5_word(words, 6), MD5_RC[6], 17);
|
||||
b = md5_op_f(b, c, d, a, md5_word(words, 7), MD5_RC[7], 22);
|
||||
a = md5_op_f(a, b, c, d, md5_word(words, 8), MD5_RC[8], 7);
|
||||
d = md5_op_f(d, a, b, c, md5_word(words, 9), MD5_RC[9], 12);
|
||||
c = md5_op_f(c, d, a, b, md5_word(words, 10), MD5_RC[10], 17);
|
||||
b = md5_op_f(b, c, d, a, md5_word(words, 11), MD5_RC[11], 22);
|
||||
a = md5_op_f(a, b, c, d, md5_word(words, 12), MD5_RC[12], 7);
|
||||
d = md5_op_f(d, a, b, c, md5_word(words, 13), MD5_RC[13], 12);
|
||||
c = md5_op_f(c, d, a, b, md5_word(words, 14), MD5_RC[14], 17);
|
||||
b = md5_op_f(b, c, d, a, md5_word(words, 15), MD5_RC[15], 22);
|
||||
|
||||
a = md5_op_g(a, b, c, d, md5_word(words, 1), MD5_RC[16], 5);
|
||||
d = md5_op_g(d, a, b, c, md5_word(words, 6), MD5_RC[17], 9);
|
||||
c = md5_op_g(c, d, a, b, md5_word(words, 11), MD5_RC[18], 14);
|
||||
b = md5_op_g(b, c, d, a, md5_word(words, 0), MD5_RC[19], 20);
|
||||
a = md5_op_g(a, b, c, d, md5_word(words, 5), MD5_RC[20], 5);
|
||||
d = md5_op_g(d, a, b, c, md5_word(words, 10), MD5_RC[21], 9);
|
||||
c = md5_op_g(c, d, a, b, md5_word(words, 15), MD5_RC[22], 14);
|
||||
b = md5_op_g(b, c, d, a, md5_word(words, 4), MD5_RC[23], 20);
|
||||
a = md5_op_g(a, b, c, d, md5_word(words, 9), MD5_RC[24], 5);
|
||||
d = md5_op_g(d, a, b, c, md5_word(words, 14), MD5_RC[25], 9);
|
||||
c = md5_op_g(c, d, a, b, md5_word(words, 3), MD5_RC[26], 14);
|
||||
b = md5_op_g(b, c, d, a, md5_word(words, 8), MD5_RC[27], 20);
|
||||
a = md5_op_g(a, b, c, d, md5_word(words, 13), MD5_RC[28], 5);
|
||||
d = md5_op_g(d, a, b, c, md5_word(words, 2), MD5_RC[29], 9);
|
||||
c = md5_op_g(c, d, a, b, md5_word(words, 7), MD5_RC[30], 14);
|
||||
b = md5_op_g(b, c, d, a, md5_word(words, 12), MD5_RC[31], 20);
|
||||
|
||||
a = md5_op_h(a, b, c, d, md5_word(words, 5), MD5_RC[32], 4);
|
||||
d = md5_op_h(d, a, b, c, md5_word(words, 8), MD5_RC[33], 11);
|
||||
c = md5_op_h(c, d, a, b, md5_word(words, 11), MD5_RC[34], 16);
|
||||
b = md5_op_h(b, c, d, a, md5_word(words, 14), MD5_RC[35], 23);
|
||||
a = md5_op_h(a, b, c, d, md5_word(words, 1), MD5_RC[36], 4);
|
||||
d = md5_op_h(d, a, b, c, md5_word(words, 4), MD5_RC[37], 11);
|
||||
c = md5_op_h(c, d, a, b, md5_word(words, 7), MD5_RC[38], 16);
|
||||
b = md5_op_h(b, c, d, a, md5_word(words, 10), MD5_RC[39], 23);
|
||||
a = md5_op_h(a, b, c, d, md5_word(words, 13), MD5_RC[40], 4);
|
||||
d = md5_op_h(d, a, b, c, md5_word(words, 0), MD5_RC[41], 11);
|
||||
c = md5_op_h(c, d, a, b, md5_word(words, 3), MD5_RC[42], 16);
|
||||
b = md5_op_h(b, c, d, a, md5_word(words, 6), MD5_RC[43], 23);
|
||||
a = md5_op_h(a, b, c, d, md5_word(words, 9), MD5_RC[44], 4);
|
||||
d = md5_op_h(d, a, b, c, md5_word(words, 12), MD5_RC[45], 11);
|
||||
c = md5_op_h(c, d, a, b, md5_word(words, 15), MD5_RC[46], 16);
|
||||
b = md5_op_h(b, c, d, a, md5_word(words, 2), MD5_RC[47], 23);
|
||||
|
||||
a = md5_op_i(a, b, c, d, md5_word(words, 0), MD5_RC[48], 6);
|
||||
d = md5_op_i(d, a, b, c, md5_word(words, 7), MD5_RC[49], 10);
|
||||
c = md5_op_i(c, d, a, b, md5_word(words, 14), MD5_RC[50], 15);
|
||||
b = md5_op_i(b, c, d, a, md5_word(words, 5), MD5_RC[51], 21);
|
||||
a = md5_op_i(a, b, c, d, md5_word(words, 12), MD5_RC[52], 6);
|
||||
d = md5_op_i(d, a, b, c, md5_word(words, 3), MD5_RC[53], 10);
|
||||
c = md5_op_i(c, d, a, b, md5_word(words, 10), MD5_RC[54], 15);
|
||||
b = md5_op_i(b, c, d, a, md5_word(words, 1), MD5_RC[55], 21);
|
||||
a = md5_op_i(a, b, c, d, md5_word(words, 8), MD5_RC[56], 6);
|
||||
d = md5_op_i(d, a, b, c, md5_word(words, 15), MD5_RC[57], 10);
|
||||
c = md5_op_i(c, d, a, b, md5_word(words, 6), MD5_RC[58], 15);
|
||||
b = md5_op_i(b, c, d, a, md5_word(words, 13), MD5_RC[59], 21);
|
||||
a = md5_op_i(a, b, c, d, md5_word(words, 4), MD5_RC[60], 6);
|
||||
d = md5_op_i(d, a, b, c, md5_word(words, 11), MD5_RC[61], 10);
|
||||
c = md5_op_i(c, d, a, b, md5_word(words, 2), MD5_RC[62], 15);
|
||||
b = md5_op_i(b, c, d, a, md5_word(words, 9), MD5_RC[63], 21);
|
||||
|
||||
a = a.wrapping_add(aa);
|
||||
b = b.wrapping_add(bb);
|
||||
c = c.wrapping_add(cc);
|
||||
d = d.wrapping_add(dd);
|
||||
}
|
||||
*state = [a, b, c, d];
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn finalize_md5_state(mut state: [u32; 4], tail: &[u8], total_bytes: u64) -> [u8; 16] {
|
||||
debug_assert!(tail.len() < 64);
|
||||
let mut pad = [0u8; 128];
|
||||
pad[..tail.len()].copy_from_slice(tail);
|
||||
pad[tail.len()] = 0x80;
|
||||
let bit_len = total_bytes.wrapping_mul(8).to_le_bytes();
|
||||
if tail.len() < 56 {
|
||||
pad[56..64].copy_from_slice(&bit_len);
|
||||
compress_md5_blocks(&mut state, pad[..64].as_chunks::<64>().0);
|
||||
} else {
|
||||
pad[120..128].copy_from_slice(&bit_len);
|
||||
compress_md5_blocks(&mut state, pad.as_chunks::<64>().0);
|
||||
}
|
||||
let mut out = [0u8; 16];
|
||||
out[0..4].copy_from_slice(&state[0].to_le_bytes());
|
||||
out[4..8].copy_from_slice(&state[1].to_le_bytes());
|
||||
out[8..12].copy_from_slice(&state[2].to_le_bytes());
|
||||
out[12..16].copy_from_slice(&state[3].to_le_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// Streaming MD5 hasher using critical-path ILP reassociation and direct slice chunking.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FastMd5 {
|
||||
state: [u32; 4],
|
||||
buf: [u8; 64],
|
||||
buf_len: usize,
|
||||
total_bytes: u64,
|
||||
}
|
||||
|
||||
impl Default for FastMd5 {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FastMd5 {
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
state: MD5_INITIAL_STATE,
|
||||
buf: [0u8; 64],
|
||||
buf_len: 0,
|
||||
total_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reset(&mut self) {
|
||||
self.state = MD5_INITIAL_STATE;
|
||||
self.buf_len = 0;
|
||||
self.total_bytes = 0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn update(&mut self, mut data: &[u8]) {
|
||||
if data.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.total_bytes = self.total_bytes.wrapping_add(data.len() as u64);
|
||||
if self.buf_len > 0 {
|
||||
let need = 64 - self.buf_len;
|
||||
if data.len() < need {
|
||||
self.buf[self.buf_len..self.buf_len + data.len()].copy_from_slice(data);
|
||||
self.buf_len += data.len();
|
||||
return;
|
||||
}
|
||||
self.buf[self.buf_len..64].copy_from_slice(&data[..need]);
|
||||
compress_md5_blocks(&mut self.state, std::slice::from_ref(&self.buf));
|
||||
self.buf_len = 0;
|
||||
data = &data[need..];
|
||||
}
|
||||
let (blocks, remainder) = data.as_chunks::<64>();
|
||||
if !blocks.is_empty() {
|
||||
compress_md5_blocks(&mut self.state, blocks);
|
||||
}
|
||||
if !remainder.is_empty() {
|
||||
self.buf[..remainder.len()].copy_from_slice(remainder);
|
||||
self.buf_len = remainder.len();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finalize_bytes(&self) -> [u8; 16] {
|
||||
finalize_md5_state(self.state, &self.buf[..self.buf_len], self.total_bytes)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finalize(self) -> [u8; 16] {
|
||||
self.finalize_bytes()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn digest(data: &[u8]) -> [u8; 16] {
|
||||
let mut state = MD5_INITIAL_STATE;
|
||||
let (blocks, remainder) = data.as_chunks::<64>();
|
||||
if !blocks.is_empty() {
|
||||
compress_md5_blocks(&mut state, blocks);
|
||||
}
|
||||
finalize_md5_state(state, remainder, data.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Magic HH-256 key: HH-256 hash of first 100 decimals of π as utf-8 with zero key.
|
||||
const MAGIC_HIGHWAY_HASH256_KEY: [u8; 32] = [
|
||||
@@ -404,7 +113,7 @@ impl HashAlgorithm {
|
||||
#[inline]
|
||||
pub fn hash_encode(&self, data: &[u8]) -> impl AsRef<[u8]> {
|
||||
match self {
|
||||
HashAlgorithm::Md5 => HashEncoded::Md5(FastMd5::digest(data)),
|
||||
HashAlgorithm::Md5 => HashEncoded::Md5(Md5::digest(data).into()),
|
||||
HashAlgorithm::HighwayHash256 => {
|
||||
let mut hasher = HighwayHasher::new(MAGIC_HIGHWAY_HASH256_PARSED_KEY);
|
||||
hasher.append(data);
|
||||
@@ -439,11 +148,11 @@ impl HashAlgorithm {
|
||||
{
|
||||
match self {
|
||||
HashAlgorithm::Md5 => {
|
||||
let mut hasher = FastMd5::new();
|
||||
let mut hasher = Md5::new();
|
||||
for slice in slices {
|
||||
hasher.update(slice);
|
||||
}
|
||||
HashEncoded::Md5(hasher.finalize_bytes())
|
||||
HashEncoded::Md5(hasher.finalize().into())
|
||||
}
|
||||
HashAlgorithm::HighwayHash256 => {
|
||||
let mut hasher = HighwayHasher::new(MAGIC_HIGHWAY_HASH256_PARSED_KEY);
|
||||
@@ -715,24 +424,4 @@ mod tests {
|
||||
let blake_hash2 = HashAlgorithm::BLAKE2b512.hash_encode(data2);
|
||||
assert_ne!(blake_hash1.as_ref(), blake_hash2.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_md5_matches_reference_across_boundaries_and_chunkings() {
|
||||
use md5::{Digest as _, Md5};
|
||||
|
||||
for len in (0..=260).chain([511, 512, 513, 1024, 4096, 8193]) {
|
||||
let data = generate_compat_test_data(len);
|
||||
let expected: [u8; 16] = Md5::digest(&data).into();
|
||||
assert_eq!(FastMd5::digest(&data), expected, "FastMd5::digest mismatch at len={len}");
|
||||
|
||||
for step in [1usize, 3, 7, 16, 31, 55, 56, 63, 64, 65, 128] {
|
||||
let mut fast = FastMd5::new();
|
||||
for chunk in data.chunks(step) {
|
||||
fast.update(chunk);
|
||||
}
|
||||
assert_eq!(fast.finalize_bytes(), expected, "FastMd5 streaming mismatch at len={len}, step={step}");
|
||||
assert_eq!(fast.finalize(), expected, "FastMd5 repeated finalize mismatch at len={len}, step={step}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,358 +694,6 @@ pub fn trim_etag(etag: &str) -> String {
|
||||
etag.trim_matches('"').to_string()
|
||||
}
|
||||
|
||||
/// Exact 64-bit SWAR byte equality mask for `b'.'` (`0x2e`) with zero cross-byte carry.
|
||||
/// Bit `7 + 8 * k` is set in the returned mask iff byte `k` of `w` equals `b'.'`.
|
||||
#[inline(always)]
|
||||
const fn swar_exact_dot_mask(w: u64) -> u64 {
|
||||
let x = w ^ 0x2e2e_2e2e_2e2e_2e2e;
|
||||
let low = (x & 0x7f7f_7f7f_7f7f_7f7f).wrapping_add(0x7f7f_7f7f_7f7f_7f7f);
|
||||
!(low | x) & 0x8080_8080_8080_8080
|
||||
}
|
||||
|
||||
/// Exact 64-bit SWAR mask for `b'.'` (`0x2e`) or `b'/'` (`0x2f`) across all byte values `0x00..=0xFF`.
|
||||
/// Since `0x2e` and `0x2f` differ only in bit 0, `(b & 0xfe) == 0x2e` holds iff `b` is `.` or `/`.
|
||||
#[inline(always)]
|
||||
const fn swar_exact_dot_or_slash_mask(w: u64) -> u64 {
|
||||
swar_exact_dot_mask(w & 0xfefe_fefe_fefe_fefe)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn is_unix_dot_or_dotdot_at(path: &[u8], dot_idx: usize) -> bool {
|
||||
if dot_idx > 0 && path[dot_idx - 1] != b'/' {
|
||||
return false;
|
||||
}
|
||||
let next = dot_idx + 1;
|
||||
if next == path.len() || path[next] == b'/' {
|
||||
return true;
|
||||
}
|
||||
path[next] == b'.' && (next + 1 == path.len() || path[next + 1] == b'/')
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn is_unix_dotdot_at(path: &[u8], dot_idx: usize) -> bool {
|
||||
if dot_idx > 0 && path[dot_idx - 1] != b'/' {
|
||||
return false;
|
||||
}
|
||||
let next = dot_idx + 1;
|
||||
next < path.len() && path[next] == b'.' && (next + 1 == path.len() || path[next + 1] == b'/')
|
||||
}
|
||||
|
||||
/// Returns `true` iff `path` (interpreted with Unix `/` separators) contains a `..` segment.
|
||||
/// Scans 8 bytes at a time using carry-free 64-bit SWAR so ordinary filenames containing dots
|
||||
/// (such as `xl.meta`, `part.1`, or `.rustfs.sys`) do not trigger component parsing.
|
||||
#[inline]
|
||||
pub const fn unix_path_has_dotdot_segment(path: &[u8]) -> bool {
|
||||
let (chunks, rem) = path.as_chunks::<8>();
|
||||
let mut c_idx = 0;
|
||||
while c_idx < chunks.len() {
|
||||
let mut mask = swar_exact_dot_mask(u64::from_le_bytes(chunks[c_idx]));
|
||||
while mask != 0 {
|
||||
let lane = (mask.trailing_zeros() >> 3) as usize;
|
||||
if is_unix_dotdot_at(path, c_idx * 8 + lane) {
|
||||
return true;
|
||||
}
|
||||
mask &= mask - 1;
|
||||
}
|
||||
c_idx += 1;
|
||||
}
|
||||
let base = chunks.len() * 8;
|
||||
let mut r_idx = 0;
|
||||
while r_idx < rem.len() {
|
||||
if rem[r_idx] == b'.' && is_unix_dotdot_at(path, base + r_idx) {
|
||||
return true;
|
||||
}
|
||||
r_idx += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn is_invalid_adjacent_dot_slash_pair(path: &[u8], idx: usize) -> bool {
|
||||
let prev = path[idx - 1];
|
||||
let curr = path[idx];
|
||||
if prev == b'/' && curr == b'/' {
|
||||
return true;
|
||||
}
|
||||
if prev == b'/' && curr == b'.' {
|
||||
return is_unix_dot_or_dotdot_at(path, idx);
|
||||
}
|
||||
if prev == b'.' && curr == b'/' {
|
||||
return is_unix_dot_or_dotdot_at(path, idx - 1);
|
||||
}
|
||||
if prev == b'.' && curr == b'.' {
|
||||
return is_unix_dotdot_at(path, idx - 1);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns `true` when `path` is a non-empty Unix path with no repeated slashes (`//`),
|
||||
/// no trailing slash (except `"/"`), and no `.` or `..` segments.
|
||||
#[inline]
|
||||
pub const fn unix_is_canonical_ascii_path(path: &[u8]) -> bool {
|
||||
if path.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if path.len() == 1 {
|
||||
return path[0] != b'.';
|
||||
}
|
||||
let first = path[0];
|
||||
let last = path[path.len() - 1];
|
||||
if last == b'/' || (first == b'.' && is_unix_dot_or_dotdot_at(path, 0)) {
|
||||
return false;
|
||||
}
|
||||
let (chunks, rem) = path.as_chunks::<8>();
|
||||
let mut c_idx = 0;
|
||||
let mut prev_hi = 0u64;
|
||||
while c_idx < chunks.len() {
|
||||
let w = u64::from_le_bytes(chunks[c_idx]);
|
||||
let m = swar_exact_dot_or_slash_mask(w);
|
||||
let mut pair_mask = m & ((m << 8) | prev_hi);
|
||||
while pair_mask != 0 {
|
||||
let lane = (pair_mask.trailing_zeros() >> 3) as usize;
|
||||
let idx = c_idx * 8 + lane;
|
||||
if idx > 0 && is_invalid_adjacent_dot_slash_pair(path, idx) {
|
||||
return false;
|
||||
}
|
||||
pair_mask &= pair_mask - 1;
|
||||
}
|
||||
prev_hi = m >> 56;
|
||||
c_idx += 1;
|
||||
}
|
||||
let base = chunks.len() * 8;
|
||||
let mut r_idx = 0;
|
||||
while r_idx < rem.len() {
|
||||
let b = rem[r_idx];
|
||||
let idx = base + r_idx;
|
||||
if (b == b'/' || b == b'.') && idx > 0 {
|
||||
let prev = path[idx - 1];
|
||||
if (prev == b'/' || prev == b'.') && is_invalid_adjacent_dot_slash_pair(path, idx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
r_idx += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn trim_trailing_slashes_and_curdir(mut bytes: &[u8]) -> &[u8] {
|
||||
while bytes.len() > 1 {
|
||||
if bytes[bytes.len() - 1] == b'/' {
|
||||
bytes = bytes.split_at(bytes.len() - 1).0;
|
||||
} else if bytes.len() >= 2 && bytes[bytes.len() - 2] == b'/' && bytes[bytes.len() - 1] == b'.' {
|
||||
if bytes.len() == 2 {
|
||||
bytes = b"/";
|
||||
break;
|
||||
}
|
||||
bytes = bytes.split_at(bytes.len() - 2).0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn strip_leading_slashes_and_curdir(mut rem: &[u8]) -> &[u8] {
|
||||
while !rem.is_empty() {
|
||||
if rem[0] == b'/' {
|
||||
rem = rem.split_at(1).1;
|
||||
} else if rem.len() == 1 && rem[0] == b'.' {
|
||||
rem = b"";
|
||||
} else if rem.len() >= 2 && rem[0] == b'.' && rem[1] == b'/' {
|
||||
rem = rem.split_at(2).1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rem
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn unix_bytes_to_os_str(bytes: &[u8]) -> Option<&std::ffi::OsStr> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
Some(std::ffi::OsStr::from_bytes(bytes))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
std::str::from_utf8(bytes).ok().map(std::ffi::OsStr::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn unix_bytes_to_path(bytes: &[u8]) -> Option<&Path> {
|
||||
unix_bytes_to_os_str(bytes).map(Path::new)
|
||||
}
|
||||
|
||||
/// Fast Unix `Path::starts_with` when both `path` and `base` have already been normalized
|
||||
/// (i.e. contain no `..` segments or leading `.`). Performs a single slice prefix comparison.
|
||||
#[inline]
|
||||
pub fn unix_normalized_path_starts_with(path: &Path, base: &Path) -> bool {
|
||||
let p = path.as_os_str().as_encoded_bytes();
|
||||
let b = trim_trailing_slashes_and_curdir(base.as_os_str().as_encoded_bytes());
|
||||
if b.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if b == b"/" {
|
||||
return p.starts_with(b"/");
|
||||
}
|
||||
if p.starts_with(b) {
|
||||
return p.len() == b.len() || p[b.len()] == b'/';
|
||||
}
|
||||
if b.contains(&b'.') || b.windows(2).any(|w| w == b"//") {
|
||||
return path.starts_with(base);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Fast Unix `Path::strip_prefix` when both `path` and `base` have already been normalized
|
||||
/// (i.e. contain no `..` segments or leading `.`). Performs a single slice prefix comparison.
|
||||
#[inline]
|
||||
pub fn unix_normalized_path_strip_prefix<'a>(path: &'a Path, base: &Path) -> Option<&'a Path> {
|
||||
let p = path.as_os_str().as_encoded_bytes();
|
||||
let b = trim_trailing_slashes_and_curdir(base.as_os_str().as_encoded_bytes());
|
||||
if b.is_empty() {
|
||||
return Some(path);
|
||||
}
|
||||
if b == b"/" {
|
||||
if !p.starts_with(b"/") {
|
||||
return None;
|
||||
}
|
||||
let mut rem = strip_leading_slashes_and_curdir(&p[1..]);
|
||||
if rem.ends_with(b"/") || rem.ends_with(b"/.") {
|
||||
rem = trim_trailing_slashes_and_curdir(rem);
|
||||
if rem == b"/" {
|
||||
rem = b"";
|
||||
}
|
||||
}
|
||||
if let Some(res) = unix_bytes_to_path(rem) {
|
||||
return Some(res);
|
||||
}
|
||||
} else if p.starts_with(b) {
|
||||
if p.len() == b.len() {
|
||||
return Some(Path::new(""));
|
||||
}
|
||||
if p[b.len()] == b'/' {
|
||||
let mut rem = strip_leading_slashes_and_curdir(&p[b.len() + 1..]);
|
||||
if rem.ends_with(b"/") || rem.ends_with(b"/.") {
|
||||
rem = trim_trailing_slashes_and_curdir(rem);
|
||||
if rem == b"/" {
|
||||
rem = b"";
|
||||
}
|
||||
}
|
||||
if let Some(res) = unix_bytes_to_path(rem) {
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if b.contains(&b'.') || b.windows(2).any(|w| w == b"//") {
|
||||
return path.strip_prefix(base).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Fast Unix `Path::parent` that slices at the last `/` via SIMD `memrchr` without scanning
|
||||
/// earlier path components.
|
||||
#[inline]
|
||||
pub fn unix_path_parent(path: &Path) -> Option<&Path> {
|
||||
let p = path.as_os_str().as_encoded_bytes();
|
||||
if p.is_empty() || p == b"/" {
|
||||
return None;
|
||||
}
|
||||
if let Some(idx) = p.iter().rposition(|&b| b == b'/') {
|
||||
if idx + 1 < p.len() {
|
||||
let tail = &p[idx + 1..];
|
||||
if tail != b"." && tail != b".." {
|
||||
if idx == 0 {
|
||||
return Some(Path::new("/"));
|
||||
}
|
||||
if !(idx == 1 && p[0] == b'.') {
|
||||
let parent_bytes = trim_trailing_slashes_and_curdir(&p[..idx]);
|
||||
if let Some(res) = unix_bytes_to_path(parent_bytes) {
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if p != b"." && p != b".." {
|
||||
return Some(Path::new(""));
|
||||
}
|
||||
path.parent()
|
||||
}
|
||||
|
||||
/// Splits a Unix `Path` into `(parent, file_name)` in a single reverse scan.
|
||||
#[inline]
|
||||
pub fn unix_split_parent_and_file_name(path: &Path) -> Option<(&Path, &std::ffi::OsStr)> {
|
||||
let p = path.as_os_str().as_encoded_bytes();
|
||||
if p.is_empty() || p == b"/" {
|
||||
return None;
|
||||
}
|
||||
if let Some(idx) = p.iter().rposition(|&b| b == b'/') {
|
||||
if idx + 1 < p.len() {
|
||||
let tail = &p[idx + 1..];
|
||||
if tail != b"." && tail != b".." {
|
||||
if idx == 0 {
|
||||
if let Some(tail_os) = unix_bytes_to_os_str(tail) {
|
||||
return Some((Path::new("/"), tail_os));
|
||||
}
|
||||
} else if !(idx == 1 && p[0] == b'.') {
|
||||
let parent_bytes = trim_trailing_slashes_and_curdir(&p[..idx]);
|
||||
if let (Some(parent_p), Some(tail_os)) = (unix_bytes_to_path(parent_bytes), unix_bytes_to_os_str(tail)) {
|
||||
return Some((parent_p, tail_os));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if p != b"."
|
||||
&& p != b".."
|
||||
&& let Some(tail_os) = unix_bytes_to_os_str(p)
|
||||
{
|
||||
return Some((Path::new(""), tail_os));
|
||||
}
|
||||
Some((path.parent()?, path.file_name()?))
|
||||
}
|
||||
|
||||
/// Verifies that `path.parent() == Some(expected_parent)` and returns `path.file_name()` in one
|
||||
/// slice prefix check.
|
||||
#[inline]
|
||||
pub fn unix_direct_child_file_name<'a>(path: &'a Path, expected_parent: &Path) -> Option<&'a std::ffi::OsStr> {
|
||||
let p = path.as_os_str().as_encoded_bytes();
|
||||
let b = trim_trailing_slashes_and_curdir(expected_parent.as_os_str().as_encoded_bytes());
|
||||
if !b.is_empty() && b != b"." && !b.starts_with(b"./") && !unix_path_has_dotdot_segment(b) {
|
||||
if b == b"/" {
|
||||
if p.starts_with(b"/") {
|
||||
let rem = strip_leading_slashes_and_curdir(&p[1..]);
|
||||
if !rem.is_empty()
|
||||
&& rem != b"."
|
||||
&& rem != b".."
|
||||
&& !rem.contains(&b'/')
|
||||
&& let Some(res) = unix_bytes_to_os_str(rem)
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
} else if p.starts_with(b) && p.len() > b.len() + 1 && p[b.len()] == b'/' {
|
||||
let rem = strip_leading_slashes_and_curdir(&p[b.len() + 1..]);
|
||||
if !rem.is_empty()
|
||||
&& rem != b"."
|
||||
&& rem != b".."
|
||||
&& !rem.contains(&b'/')
|
||||
&& let Some(res) = unix_bytes_to_os_str(rem)
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
if path.parent() != Some(expected_parent) {
|
||||
return None;
|
||||
}
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` contains a `..` component (parent directory traversal).
|
||||
fn contains_parent_dir_component(path: &str) -> bool {
|
||||
path.split(['/', '\\']).any(|component| component == "..")
|
||||
@@ -1563,110 +1211,4 @@ mod tests {
|
||||
assert!(normalize_extract_entry_key("file.txt", Some("../bad"), false).is_err());
|
||||
assert!(normalize_extract_entry_key("/absolute", None, false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unix_swar_path_helpers_match_std_path_across_boundaries() {
|
||||
for pad in 1..=20 {
|
||||
let prefix = "a".repeat(pad);
|
||||
let clean_cases = [
|
||||
format!("/{prefix}/bucket/object/xl.meta"),
|
||||
format!("/{prefix}/.rustfs.sys/tmp/uuid/part.1"),
|
||||
format!("/{prefix}/dir..name/..hidden/file...ext"),
|
||||
format!("{prefix}/relative/object.tar.gz"),
|
||||
];
|
||||
for case in &clean_cases {
|
||||
let bytes = case.as_bytes();
|
||||
assert!(!unix_path_has_dotdot_segment(bytes), "clean case flagged dotdot: {case}");
|
||||
assert!(unix_is_canonical_ascii_path(bytes), "canonical case rejected: {case}");
|
||||
}
|
||||
|
||||
let non_canonical_slash_cases = [
|
||||
format!("//{prefix}/bucket/object/xl.meta"),
|
||||
format!("/{prefix}//bucket/object/xl.meta"),
|
||||
format!("/{prefix}/bucket/object/"),
|
||||
];
|
||||
for case in &non_canonical_slash_cases {
|
||||
assert!(
|
||||
!unix_is_canonical_ascii_path(case.as_bytes()),
|
||||
"non-canonical slash case accepted: {case}"
|
||||
);
|
||||
}
|
||||
|
||||
let dot_cases = [
|
||||
format!("/{prefix}/./xl.meta"),
|
||||
format!("/{prefix}/bucket/."),
|
||||
format!("./{prefix}/bucket"),
|
||||
".".to_string(),
|
||||
];
|
||||
for case in &dot_cases {
|
||||
let bytes = case.as_bytes();
|
||||
assert!(!unix_path_has_dotdot_segment(bytes), "dot-only case flagged as dotdot: {case}");
|
||||
assert!(!unix_is_canonical_ascii_path(bytes), "dot case accepted as canonical: {case}");
|
||||
}
|
||||
|
||||
let dotdot_cases = [
|
||||
format!("/{prefix}/../xl.meta"),
|
||||
format!("/{prefix}/bucket/.."),
|
||||
format!("../{prefix}/bucket"),
|
||||
"..".to_string(),
|
||||
];
|
||||
for case in &dotdot_cases {
|
||||
let bytes = case.as_bytes();
|
||||
assert!(unix_path_has_dotdot_segment(bytes), "dotdot case missed by dotdot: {case}");
|
||||
assert!(!unix_is_canonical_ascii_path(bytes), "dotdot case accepted as canonical: {case}");
|
||||
}
|
||||
}
|
||||
|
||||
let base = Path::new("/tmp/rustfs-disk1");
|
||||
let child = Path::new("/tmp/rustfs-disk1/bucket/prefix/xl.meta");
|
||||
let sibling = Path::new("/tmp/rustfs-disk10/bucket/prefix/xl.meta");
|
||||
assert!(unix_normalized_path_starts_with(child, base));
|
||||
assert!(!unix_normalized_path_starts_with(sibling, base));
|
||||
assert_eq!(unix_normalized_path_strip_prefix(child, base), Some(Path::new("bucket/prefix/xl.meta")));
|
||||
assert_eq!(unix_normalized_path_strip_prefix(base, base), Some(Path::new("")));
|
||||
assert_eq!(unix_normalized_path_strip_prefix(sibling, base), None);
|
||||
assert_eq!(unix_path_parent(child), Some(Path::new("/tmp/rustfs-disk1/bucket/prefix")));
|
||||
assert_eq!(unix_path_parent(Path::new("/")), None);
|
||||
assert_eq!(unix_path_parent(Path::new("/rootchild")), Some(Path::new("/")));
|
||||
assert_eq!(unix_path_parent(Path::new("relative")), Some(Path::new("")));
|
||||
assert_eq!(
|
||||
unix_split_parent_and_file_name(child),
|
||||
Some((Path::new("/tmp/rustfs-disk1/bucket/prefix"), std::ffi::OsStr::new("xl.meta")))
|
||||
);
|
||||
assert_eq!(
|
||||
unix_direct_child_file_name(child, Path::new("/tmp/rustfs-disk1/bucket/prefix")),
|
||||
Some(std::ffi::OsStr::new("xl.meta"))
|
||||
);
|
||||
assert_eq!(unix_direct_child_file_name(child, base), None);
|
||||
|
||||
let proc_root = Path::new("/proc/self/fd/12/.");
|
||||
let proc_bucket = Path::new("/proc/self/fd/12/./bucket");
|
||||
let proc_obj = Path::new("/proc/self/fd/12/./bucket/prefix/xl.meta");
|
||||
assert!(unix_normalized_path_starts_with(proc_bucket, proc_root));
|
||||
assert!(unix_normalized_path_starts_with(proc_obj, proc_bucket));
|
||||
assert!(unix_normalized_path_starts_with(proc_obj, proc_root));
|
||||
assert_eq!(
|
||||
unix_normalized_path_strip_prefix(proc_obj, proc_root),
|
||||
Some(Path::new("bucket/prefix/xl.meta"))
|
||||
);
|
||||
assert_eq!(
|
||||
unix_normalized_path_strip_prefix(proc_obj, proc_bucket),
|
||||
Some(Path::new("prefix/xl.meta"))
|
||||
);
|
||||
assert_eq!(unix_path_parent(proc_obj), Some(Path::new("/proc/self/fd/12/./bucket/prefix")));
|
||||
assert_eq!(
|
||||
unix_direct_child_file_name(proc_obj, Path::new("/proc/self/fd/12/./bucket/prefix")),
|
||||
Some(std::ffi::OsStr::new("xl.meta"))
|
||||
);
|
||||
|
||||
assert!(unix_is_canonical_ascii_path("bucket/对象-ключ-é/xl.meta".as_bytes()));
|
||||
assert!(unix_normalized_path_starts_with(
|
||||
Path::new("/data/rustfs/bucket/dir/xl.meta"),
|
||||
Path::new("/data/rustfs/bucket//dir")
|
||||
));
|
||||
assert_eq!(
|
||||
unix_normalized_path_strip_prefix(Path::new("/a/b/"), Path::new("/a")),
|
||||
Some(Path::new("b"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
version = "0.13.4"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -6,6 +6,6 @@ This private package satisfies the version constraints of Avro, Arrow IPC, Parqu
|
||||
|
||||
The fixed implementation closes RUSTSEC-2026-0391, RUSTSEC-2026-0398, and RUSTSEC-2026-0403. A successful advisory scan alone is insufficient evidence: cargo-deny skips local path packages. The dependency-graph test therefore requires this adapter's sole runtime dependency to be the exact fixed registry implementation and rejects vulnerable registry codec versions. Security tests exercise abandoned stream contexts, malformed legacy frames, and seekable offsets past EOF. Consumer tests retain zstandard support in all four upstream formats.
|
||||
|
||||
Run `cargo nextest run --locked -p zstd@0.13.3` for the focused tests. The seekable feature is enabled by a test-only dependency to cover the optional vulnerable API without enabling it in production.
|
||||
Run `cargo nextest run --locked -p zstd@0.13.4` for the focused tests. The seekable feature is enabled by a test-only dependency to cover the optional vulnerable API without enabling it in production.
|
||||
|
||||
Remove this package and the root Cargo patch when all consumers accept the fixed zstd release line directly. The cleanup marker is registered in the architecture compatibility register.
|
||||
|
||||
@@ -57,7 +57,7 @@ fn legacy_constraints_resolve_only_to_the_fixed_registry_implementation() {
|
||||
let version = Version::parse(package["version"].as_str().expect("package version")).expect("valid package semver");
|
||||
if name == "zstd" && package.get("source").is_none() {
|
||||
adapters += 1;
|
||||
assert_eq!(version, Version::new(0, 13, 3));
|
||||
assert_eq!(version, Version::new(0, 13, 4));
|
||||
let runtime = package["dependencies"].as_array().expect("adapter dependency edges");
|
||||
assert!(runtime.iter().any(|value| value.as_str() == Some("zstd 0.14.3")));
|
||||
} else {
|
||||
|
||||
@@ -36,10 +36,6 @@ allow-git = [
|
||||
# Official s3s repository pinned for Iceberg SigV4 path encoding support.
|
||||
# owner: GatewayJ review: 2026-10
|
||||
"https://github.com/s3s-project/s3s",
|
||||
# Upstream OIDC/OAuth2 crypto migrations, pinned to reviewed PR commits.
|
||||
# owner: rustfs-maintainers review: 2026-10
|
||||
"https://github.com/ramosbugs/openidconnect-rs",
|
||||
"https://github.com/ramosbugs/oauth2-rs",
|
||||
# Completion ownership patch fixed to rustfs/uring#20.
|
||||
# owner: overtrue review: 2026-10
|
||||
"https://github.com/rustfs/uring.git",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
## Open Items
|
||||
|
||||
- `openidconnect-hmac-signature-width` ID token verification guard: openidconnect 4.1.0 converts decoded HMAC signatures to fixed-size arrays, which panic on unexpected lengths. Check HS256, HS384, and HS512 signature widths before delegating all cryptographic, issuer, audience, expiry, and nonce checks to the upstream verifier. Remove the guard after a supported upstream release returns verification errors for arbitrary HMAC signature lengths.
|
||||
|
||||
- `zstd-013-fixed-backend` dependency constraint bridge: Avro, Arrow IPC, Parquet, and Kafka still require zstd 0.13. The local adapter re-exports the fixed registry zstd 0.14.3 implementation and its zstd-safe types. Remove the adapter and Cargo patch after every dependency accepts the fixed zstd line without the 0.13 bridge.
|
||||
|
||||
- `8565` historical rebalance replacement recovery: older schema-seven intents can exhaust their retry budget before format repair when retained rebalance metadata describes removed pools. After validating blank targets, terminal historical topology, and ownership, grant one durable handoff and one rebuild attempt. Remove after supported direct upgrades no longer admit snapshots lacking replacement_preformat_handoff_retry_done and all retained affected intents have been resolved.
|
||||
|
||||
Generated
+9
-10
@@ -273,9 +273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.156.0"
|
||||
version = "1.157.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1072c9aa20159b9b417115342d38c4c53108fc98e5359fcb86b7d4575747eb59"
|
||||
checksum = "6ea9794c741520d8baec9462bcd0ad12164d4f5f78d008a8da9d6a0b7222caa8"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -1837,18 +1837,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hotpath"
|
||||
version = "0.30.2"
|
||||
version = "0.30.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39e941010d86063ff233bf8325269bf68097fb24ba67000fbddc0a410e8056e"
|
||||
checksum = "d9632c1fd213e63c1b88d8f3e59543da4999b17f859cc1e1d67cf8f2207e8935"
|
||||
dependencies = [
|
||||
"hotpath-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-macros"
|
||||
version = "0.30.2"
|
||||
version = "0.30.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a24f5179ff680132003306354efd0ac8cde12e87b1a788719c836e91af35cb58"
|
||||
checksum = "5d92fdfec803932e5764ea93c201cf7c6c2d8cff1871da44f64f5094c9c3ef8a"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
@@ -2418,9 +2418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
@@ -4134,8 +4134,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustfs-uring"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7deda770b7981ee3bfaa202ecd18625fc794c962b8d58cd4f436952b7e66dcf"
|
||||
source = "git+https://github.com/rustfs/uring.git?rev=eb8fc160e71d804427de1e905f5c226d580f71bf#eb8fc160e71d804427de1e905f5c226d580f71bf"
|
||||
dependencies = [
|
||||
"io-uring",
|
||||
"libc",
|
||||
|
||||
Reference in New Issue
Block a user