You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/09/19 18:11:27 UTC
(opendal-yinyang) 01/01: feat(core): persist and read file contents
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a commit to branch xuanwo/file-content
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git
commit eeb8caa272faec5d1890b1a77fffcbebaf8be279
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Sun Sep 20 02:11:16 2026 +0800
feat(core): persist and read file contents
Co-authored-by: Chojan Shang <ps...@apache.org>
---
.github/workflows/ci.yml | 2 -
Cargo.lock | 1206 +++++++++++++++++++++++++++++++++++++++-
Cargo.toml | 1 +
crates/core/Cargo.toml | 9 +-
crates/core/src/content.rs | 197 +++++++
crates/core/src/error.rs | 5 +
crates/core/src/lib.rs | 1 +
crates/core/src/publication.rs | 4 +-
crates/core/tests/core.rs | 307 +++++++++-
specs/yinyang-format.md | 29 +
10 files changed, 1747 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3547ac7..27dd4a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,8 +19,6 @@ name: Apache OpenDALâ„¢ YinYang CI
on:
pull_request:
- branches:
- - main
permissions:
contents: read
diff --git a/Cargo.lock b/Cargo.lock
index 21bd158..5b54d65 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,7 +38,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -70,10 +70,45 @@ version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0"
dependencies = [
- "hashbrown",
+ "hashbrown 0.17.1",
"slab",
]
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "base64"
version = "0.23.1"
@@ -86,6 +121,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
[[package]]
name = "blake3"
version = "1.8.7"
@@ -137,6 +178,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
@@ -152,6 +195,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core",
+]
+
[[package]]
name = "clap"
version = "4.6.6"
@@ -192,24 +246,85 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
+]
+
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
[[package]]
name = "cpufeatures"
version = "0.3.1"
@@ -219,6 +334,32 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crc-fast"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5"
+dependencies = [
+ "digest 0.10.7",
+ "spin",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
[[package]]
name = "crypto-common"
version = "0.2.2"
@@ -228,13 +369,22 @@ dependencies = [
"hybrid-array",
]
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"defmt-macros",
]
@@ -259,6 +409,15 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "crypto-common 0.1.7",
+]
+
[[package]]
name = "digest"
version = "0.11.3"
@@ -267,7 +426,8 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
- "crypto-common",
+ "crypto-common 0.2.2",
+ "ctutils",
]
[[package]]
@@ -281,6 +441,21 @@ dependencies = [
"syn 3.0.3",
]
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
@@ -296,6 +471,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "futures"
version = "0.3.34"
@@ -304,6 +485,7 @@ checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [
"futures-channel",
"futures-core",
+ "futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -326,6 +508,17 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
[[package]]
name = "futures-io"
version = "0.3.34"
@@ -372,6 +565,29 @@ dependencies = [
"slab",
]
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
[[package]]
name = "getrandom"
version = "0.4.3"
@@ -379,10 +595,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi",
+ "rand_core",
+ "wasm-bindgen",
]
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -395,6 +620,21 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest 0.11.3",
+]
+
[[package]]
name = "http"
version = "1.5.0"
@@ -405,6 +645,35 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
[[package]]
name = "hybrid-array"
version = "0.4.14"
@@ -414,6 +683,64 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "hyper"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -518,6 +845,12 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -585,6 +918,65 @@ dependencies = [
"jiff-tzdb",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.104"
@@ -614,6 +1006,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+[[package]]
+name = "lru-slab"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
+
[[package]]
name = "md-5"
version = "0.11.0"
@@ -621,7 +1019,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
dependencies = [
"cfg-if",
- "digest",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "mea"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c709842c4ce65cb91e2666ad5319dfc1efc3af0d34f02075eddca9000d9f8afb"
+dependencies = [
+ "hashbrown 0.17.1",
+ "slab",
]
[[package]]
@@ -630,6 +1038,17 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+[[package]]
+name = "mio"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -649,6 +1068,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33dbff14cc9bb085224256d6a81289d2f3202e85b06f408d42534b42162a4231"
dependencies = [
"opendal-core",
+ "opendal-http-transport-reqwest",
+ "opendal-service-s3",
]
[[package]]
@@ -659,7 +1080,7 @@ checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7"
dependencies = [
"anyhow",
"asyncband",
- "base64",
+ "base64 0.23.1",
"bytes",
"futures",
"http",
@@ -668,6 +1089,7 @@ dependencies = [
"md-5",
"percent-encoding",
"quick-xml",
+ "reqsign-core",
"serde",
"serde_json",
"tokio",
@@ -676,6 +1098,57 @@ dependencies = [
"web-time",
]
+[[package]]
+name = "opendal-http-transport-reqwest"
+version = "0.58.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85663452ea32bbc17e8f79ab29788c846d116ec7de31451be9c787e462dcb36c"
+dependencies = [
+ "bytes",
+ "futures",
+ "http",
+ "http-body",
+ "opendal-core",
+ "reqwest",
+]
+
+[[package]]
+name = "opendal-service-s3"
+version = "0.58.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64335f9f24ccb62ac36f1d976342b48611a75ba61979813a4f78a4ebd94de42"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "crc-fast",
+ "http",
+ "log",
+ "md-5",
+ "opendal-core",
+ "quick-xml",
+ "reqsign-aws-v4",
+ "reqsign-core",
+ "reqsign-file-read-tokio",
+ "serde",
+ "url",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -688,6 +1161,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
[[package]]
name = "portable-atomic"
version = "1.15.0"
@@ -731,6 +1210,63 @@ dependencies = [
"serde",
]
+[[package]]
+name = "quinn"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "quote"
version = "1.0.47"
@@ -746,12 +1282,312 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "reqsign-aws-core"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bac4749b7dfa7bfaccd01eb03e9dc795ed37e3f20d6f0f38e2c67ee85ad6bc86"
+dependencies = [
+ "bytes",
+ "form_urlencoded",
+ "hex",
+ "http",
+ "log",
+ "percent-encoding",
+ "quick-xml",
+ "reqsign-core",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sha1",
+]
+
+[[package]]
+name = "reqsign-aws-v4"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff250f0fd0b913fbd565e405acc553da0f13bde30bfb5403178c9d0313cdc15f"
+dependencies = [
+ "bytes",
+ "http",
+ "log",
+ "quick-xml",
+ "reqsign-aws-core",
+ "reqsign-core",
+ "serde",
+]
+
+[[package]]
+name = "reqsign-core"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff052daffb0599681c50f85c59e7236438976efe991ab864edd9f3b235501a0f"
+dependencies = [
+ "anyhow",
+ "base64 0.23.1",
+ "bytes",
+ "futures",
+ "hex",
+ "hmac",
+ "http",
+ "jiff",
+ "log",
+ "mea",
+ "percent-encoding",
+ "sha1",
+ "sha2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "reqsign-file-read-tokio"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3235df90a6bca681aa47dd86f2393d122a6d77042aa8a7c81e218cd45c5bfc0"
+dependencies = [
+ "anyhow",
+ "reqsign-core",
+ "tokio",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -795,12 +1631,62 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.11.3",
+]
+
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "slab"
version = "0.4.12"
@@ -813,6 +1699,22 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -825,6 +1727,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "2.0.119"
@@ -847,6 +1755,15 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -878,6 +1795,15 @@ dependencies = [
"syn 3.0.3",
]
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "tinystr"
version = "0.8.4"
@@ -910,8 +1836,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
+ "libc",
+ "mio",
"pin-project-lite",
+ "socket2",
"tokio-macros",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -925,6 +1855,99 @@ dependencies = [
"syn 3.0.3",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
[[package]]
name = "typenum"
version = "1.20.1"
@@ -952,6 +1975,12 @@ dependencies = [
"tinyvec",
]
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -982,12 +2011,43 @@ version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
- "getrandom",
+ "getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
]
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
@@ -1001,6 +2061,16 @@ dependencies = [
"wasm-bindgen-shared",
]
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
@@ -1033,6 +2103,29 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "web-time"
version = "1.1.0"
@@ -1043,6 +2136,15 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "which"
version = "8.0.5"
@@ -1052,12 +2154,30 @@ dependencies = [
"libc",
]
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -1067,6 +2187,70 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
[[package]]
name = "writeable"
version = "0.6.4"
@@ -1143,6 +2327,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
[[package]]
name = "zerotrie"
version = "0.2.5"
diff --git a/Cargo.toml b/Cargo.toml
index ce7227f..e209a37 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,7 @@ borsh = { version = "1.8.1", default-features = false, features = ["std"] }
clap = { version = "4.6.5", features = ["derive"] }
futures-util = "0.3"
opendal = { version = "0.58.2", default-features = false }
+tokio = { version = "1", features = ["io-util"] }
unicode-casefold = "0.2"
unicode-normalization = "0.1"
uuid = { version = "1", features = ["v4"] }
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 9b54dac..497f6e1 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -30,13 +30,18 @@ blake3.workspace = true
borsh.workspace = true
futures-util.workspace = true
opendal.workspace = true
+tokio.workspace = true
unicode-casefold.workspace = true
unicode-normalization.workspace = true
uuid.workspace = true
[dev-dependencies]
-opendal = { workspace = true, features = ["services-memory"] }
-tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+opendal = { workspace = true, features = [
+ "http-transport-reqwest",
+ "services-memory",
+ "services-s3",
+] }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
[lints]
workspace = true
diff --git a/crates/core/src/content.rs b/crates/core/src/content.rs
new file mode 100644
index 0000000..a2fc81f
--- /dev/null
+++ b/crates/core/src/content.rs
@@ -0,0 +1,197 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::BTreeMap;
+
+use futures_util::TryStreamExt as _;
+use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
+
+use crate::{BlobRef, ContentId, Error, File, FilePart, Fs, NodeBody, Result, Tree};
+
+const DATA_PREFIX: &str = ".yinyang/data/";
+const BUFFER_BYTES: usize = 256 * 1024;
+
+impl Fs {
+ /// Upload a complete file without publishing a namespace change.
+ ///
+ /// Returns only after the backend acknowledges the immutable object.
+ /// Failed or cancelled uploads can leave unreachable data. Empty files do
+ /// not allocate an object.
+ pub async fn write_file(&self, source: &mut (impl AsyncRead + Unpin)) -> Result<File> {
+ let mut bytes = vec![0; BUFFER_BYTES];
+ let first = source
+ .read(&mut bytes)
+ .await
+ .map_err(|error| Error::from_io("read YinYang file source", error))?;
+ if first == 0 {
+ return File::new(ContentId::new(blake3::hash(&[]).into(), 0), Vec::new());
+ }
+ let path = format!("{DATA_PREFIX}{}", uuid::Uuid::new_v4().simple());
+ let mut writer = self
+ .operator
+ .writer_with(&path)
+ .if_not_exists(true)
+ .await
+ .map_err(|error| Error::from_storage("write YinYang file", error))?;
+ let result = async {
+ let mut hasher = blake3::Hasher::new();
+ let mut length = 0_u64;
+ let mut count = first;
+ loop {
+ length = length
+ .checked_add(count as u64)
+ .ok_or_else(|| Error::invalid("write YinYang file", "file length overflows"))?;
+ hasher.update(&bytes[..count]);
+ writer
+ .write(bytes[..count].to_vec())
+ .await
+ .map_err(|error| Error::from_storage("write YinYang file", error))?;
+ count = source
+ .read(&mut bytes)
+ .await
+ .map_err(|error| Error::from_io("read YinYang file source", error))?;
+ if count == 0 {
+ break;
+ }
+ }
+ writer
+ .close()
+ .await
+ .map_err(|error| Error::from_storage("close YinYang file", error))?;
+ let content = ContentId::new(hasher.finalize().into(), length);
+ File::new(
+ content,
+ vec![FilePart::new(0..length, 0, BlobRef::new(path, content))?],
+ )
+ }
+ .await;
+ if result.is_err() {
+ let _ = writer.abort().await;
+ }
+ result
+ }
+
+ /// Copy and verify a complete file with bounded transfer buffers.
+ ///
+ /// Every referenced blob is read in full, including bytes outside a part,
+ /// because its digest covers the complete blob. The assembled logical
+ /// file is checked separately. Bytes delivered before this method returns
+ /// are provisional: on any error the caller must discard the destination.
+ /// This method does not flush, close, or sync the caller's destination.
+ pub async fn read_file(
+ &self,
+ file: &File,
+ destination: &mut (impl AsyncWrite + Unpin),
+ ) -> Result<()> {
+ let mut logical_hasher = blake3::Hasher::new();
+ for part in file.parts() {
+ let reference = part.blob();
+ let path = data_path(reference)?;
+ let reader = self
+ .operator
+ .reader_with(path)
+ .chunk(BUFFER_BYTES)
+ .await
+ .map_err(read_error)?;
+ let mut stream = reader.into_stream(..).await.map_err(read_error)?;
+ let mut hasher = blake3::Hasher::new();
+ let mut offset = 0_u64;
+ let start = part.blob_offset();
+ let end = start + (part.range().end - part.range().start);
+ while let Some(buffer) = stream.try_next().await.map_err(read_error)? {
+ for bytes in buffer {
+ let next = offset
+ .checked_add(bytes.len() as u64)
+ .filter(|next| *next <= reference.content().length())
+ .ok_or_else(|| {
+ Error::corrupt("read YinYang file", "blob exceeds its declared length")
+ })?;
+ hasher.update(&bytes);
+ let copy_start = start.max(offset);
+ let copy_end = end.min(next);
+ if copy_start < copy_end {
+ let selected =
+ &bytes[(copy_start - offset) as usize..(copy_end - offset) as usize];
+ logical_hasher.update(selected);
+ destination.write_all(selected).await.map_err(|error| {
+ Error::from_io("write YinYang file destination", error)
+ })?;
+ }
+ offset = next;
+ }
+ }
+ if ContentId::new(hasher.finalize().into(), offset) != reference.content() {
+ return Err(Error::corrupt(
+ "read YinYang file",
+ "blob does not match its reference",
+ ));
+ }
+ }
+ if logical_hasher.finalize().as_bytes() != file.content().digest() {
+ return Err(Error::corrupt(
+ "read YinYang file",
+ "logical content digest does not match",
+ ));
+ }
+ Ok(())
+ }
+
+ pub(crate) async fn verify_new_files(&self, previous: &Tree, next: &Tree) -> Result<()> {
+ let previous = previous
+ .iter()
+ .map(|(_, node)| (node.id(), node))
+ .collect::<BTreeMap<_, _>>();
+ for (_, node) in next.iter() {
+ if let NodeBody::File(file) = node.body() {
+ if previous
+ .get(&node.id())
+ .is_some_and(|old| old.body() == node.body())
+ {
+ continue;
+ }
+ self.read_file(file, &mut tokio::io::sink()).await?;
+ }
+ }
+ Ok(())
+ }
+}
+
+fn data_path(reference: &BlobRef) -> Result<&str> {
+ let path = std::str::from_utf8(reference.as_bytes())
+ .map_err(|_| Error::corrupt("read YinYang file", "invalid data reference"))?;
+ let valid = path.strip_prefix(DATA_PREFIX).is_some_and(|id| {
+ id.len() == 32
+ && id
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ });
+ if !valid {
+ return Err(Error::corrupt(
+ "read YinYang file",
+ "invalid data reference",
+ ));
+ }
+ Ok(path)
+}
+
+fn read_error(error: opendal::Error) -> Error {
+ if error.kind() == opendal::ErrorKind::NotFound {
+ Error::corrupt("read YinYang file", "referenced data is missing")
+ } else {
+ Error::from_storage("read YinYang file", error)
+ }
+}
diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs
index fb76738..5855a67 100644
--- a/crates/core/src/error.rs
+++ b/crates/core/src/error.rs
@@ -25,6 +25,7 @@ pub enum ErrorKind {
NotFound,
Unsupported,
Storage,
+ Io,
}
/// Error returned by the YinYang Format core.
@@ -71,6 +72,10 @@ impl Error {
pub(crate) fn from_storage(operation: &'static str, error: opendal::Error) -> Self {
Self::new(ErrorKind::Storage, operation, error.to_string())
}
+
+ pub(crate) fn from_io(operation: &'static str, error: std::io::Error) -> Self {
+ Self::new(ErrorKind::Io, operation, error.to_string())
+ }
}
impl fmt::Display for Error {
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index 028753b..55ad402 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -17,6 +17,7 @@
//! YinYang Format values, OpenDAL persistence, and publication state machine.
+mod content;
mod error;
mod filesystem;
mod identity;
diff --git a/crates/core/src/publication.rs b/crates/core/src/publication.rs
index 39799b3..f7ebb34 100644
--- a/crates/core/src/publication.rs
+++ b/crates/core/src/publication.rs
@@ -52,7 +52,7 @@ pub enum CommitOutcome {
#[derive(Debug)]
pub struct Fs {
root: NodeId,
- operator: opendal::Operator,
+ pub(crate) operator: opendal::Operator,
}
impl Fs {
@@ -137,6 +137,8 @@ impl Fs {
.version
.tree()
.validate_successor(version.tree(), self.root)?;
+ self.verify_new_files(observed.tree(), version.tree())
+ .await?;
let number = version.number();
let blob = write_version(&self.operator, &version).await?;
match replace_head(&self.operator, &observed.head, &blob).await {
diff --git a/crates/core/tests/core.rs b/crates/core/tests/core.rs
index ae5926e..0ec0d4e 100644
--- a/crates/core/tests/core.rs
+++ b/crates/core/tests/core.rs
@@ -38,6 +38,8 @@ struct TestState {
fail_after_head_write: bool,
stat_calls: u64,
version_write_calls: u64,
+ fail_data_close: bool,
+ data_aborts: u64,
}
#[derive(Clone, Debug)]
@@ -274,6 +276,12 @@ impl oio::Write for TestWriter {
async fn close(&mut self) -> opendal::Result<Metadata> {
let mut state = self.state.lock().unwrap();
+ if self.path.starts_with(".yinyang/data/") && state.fail_data_close {
+ return Err(opendal::Error::new(
+ opendal::ErrorKind::Unexpected,
+ "data close failed",
+ ));
+ }
let current = state.objects.get(&self.path);
if self.if_not_exists && current.is_some() {
return Err(opendal::Error::new(
@@ -310,6 +318,9 @@ impl oio::Write for TestWriter {
}
async fn abort(&mut self) -> opendal::Result<()> {
+ if self.path.starts_with(".yinyang/data/") {
+ self.state.lock().unwrap().data_aborts += 1;
+ }
self.bytes.clear();
Ok(())
}
@@ -362,6 +373,300 @@ fn advance_root_membership(tree: &mut Tree) {
);
}
+fn add_file(tree: &Tree, name: &str, file: File) -> Tree {
+ let mut tree = tree.clone();
+ advance_root_membership(&mut tree);
+ tree.insert(
+ Path::new(name).unwrap(),
+ Node::file(NodeId::generate(), Generation::FIRST, false, file),
+ );
+ tree
+}
+
+#[tokio::test]
+async fn persists_file_bytes_across_reopen_and_old_versions() {
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ let bytes = (0..900_000).map(|i| (i % 251) as u8).collect::<Vec<_>>();
+ let file = fs.write_file(&mut bytes.as_slice()).await.unwrap();
+ let initial = fs.observe().await.unwrap();
+ let id = CommitId::generate();
+ let tree = add_file(initial.tree(), "large", file.clone());
+ backend.fail_next_head_write_after_success();
+ assert_eq!(
+ fs.commit(&initial, id, tree.clone()).await.unwrap(),
+ CommitOutcome::Committed { version: 1 }
+ );
+ assert_eq!(
+ fs.commit(&initial, id, tree).await.unwrap(),
+ CommitOutcome::Committed { version: 1 }
+ );
+ let reopened = Fs::open(backend.operator()).await.unwrap();
+ let observed = reopened.observe().await.unwrap();
+ let NodeBody::File(persisted) = observed
+ .tree()
+ .get(&Path::new("large").unwrap())
+ .unwrap()
+ .body()
+ else {
+ panic!()
+ };
+ let mut output = Vec::new();
+ reopened.read_file(persisted, &mut output).await.unwrap();
+ assert_eq!(output, bytes);
+ let replacement = fs.write_file(&mut &b"new"[..]).await.unwrap();
+ let old_node = observed.tree().get(&Path::new("large").unwrap()).unwrap();
+ let mut next = observed.tree().clone();
+ next.insert(
+ Path::new("large").unwrap(),
+ Node::file(
+ old_node.id(),
+ old_node.generation().next().unwrap(),
+ false,
+ replacement,
+ ),
+ );
+ fs.commit(&observed, CommitId::generate(), next)
+ .await
+ .unwrap();
+ output.clear();
+ reopened.read_file(&file, &mut output).await.unwrap();
+ assert_eq!(output, bytes);
+}
+
+#[tokio::test]
+async fn reads_parts_offsets_and_checks_the_logical_digest() {
+ let fs = Fs::create(TestBackend::default().operator()).await.unwrap();
+ let source = fs.write_file(&mut &b"0123456789"[..]).await.unwrap();
+ let blob = source.parts()[0].blob().clone();
+ let parts = vec![
+ FilePart::new(0..3, 6, blob.clone()).unwrap(),
+ FilePart::new(3..5, 1, blob).unwrap(),
+ ];
+ let file = File::new(
+ ContentId::new(blake3::hash(b"67812").into(), 5),
+ parts.clone(),
+ )
+ .unwrap();
+ let mut output = Vec::new();
+ fs.read_file(&file, &mut output).await.unwrap();
+ assert_eq!(output, b"67812");
+ let wrong = File::new(ContentId::new([0; 32], 5), parts).unwrap();
+ assert_eq!(
+ fs.read_file(&wrong, &mut tokio::io::sink())
+ .await
+ .unwrap_err()
+ .kind(),
+ ErrorKind::Corrupt
+ );
+}
+
+#[tokio::test]
+async fn empty_files_need_no_data_objects() {
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ let empty = fs.write_file(&mut &b""[..]).await.unwrap();
+ assert!(empty.parts().is_empty());
+ fs.read_file(&empty, &mut tokio::io::sink()).await.unwrap();
+ assert!(
+ !backend
+ .state
+ .lock()
+ .unwrap()
+ .objects
+ .keys()
+ .any(|path| path.starts_with(".yinyang/data/"))
+ );
+}
+
+#[tokio::test]
+async fn missing_or_corrupt_data_cannot_be_published() {
+ for corruption in ["missing", "digest", "short", "long"] {
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ let file = fs.write_file(&mut &b"content"[..]).await.unwrap();
+ let path = std::str::from_utf8(file.parts()[0].blob().as_bytes()).unwrap();
+ {
+ let mut state = backend.state.lock().unwrap();
+ if corruption == "missing" {
+ state.objects.remove(path);
+ } else {
+ let bytes = &mut state.objects.get_mut(path).unwrap().bytes;
+ match corruption {
+ "digest" => bytes[0] ^= 1,
+ "short" => {
+ bytes.pop();
+ }
+ _ => bytes.push(0),
+ }
+ }
+ }
+ let observed = fs.observe().await.unwrap();
+ let error = fs
+ .commit(
+ &observed,
+ CommitId::generate(),
+ add_file(observed.tree(), "bad", file),
+ )
+ .await
+ .unwrap_err();
+ assert_eq!(error.kind(), ErrorKind::Corrupt, "{corruption}: {error}");
+ assert_eq!(fs.observe().await.unwrap(), observed);
+ }
+}
+
+#[tokio::test]
+async fn failed_upload_aborts_and_never_changes_head() {
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ let observed = fs.observe().await.unwrap();
+ backend.state.lock().unwrap().fail_data_close = true;
+ assert_eq!(
+ fs.write_file(&mut &b"content"[..])
+ .await
+ .unwrap_err()
+ .kind(),
+ ErrorKind::Storage
+ );
+ assert_eq!(backend.state.lock().unwrap().data_aborts, 1);
+ assert_eq!(fs.observe().await.unwrap(), observed);
+}
+
+#[tokio::test]
+async fn invalid_data_locations_are_rejected() {
+ let fs = Fs::create(TestBackend::default().operator()).await.unwrap();
+ for path in [".yinyang/head", ".yinyang/data/../head", "external"] {
+ let content = ContentId::new(blake3::hash(b"x").into(), 1);
+ let file = File::new(
+ content,
+ vec![FilePart::new(0..1, 0, BlobRef::new(path, content)).unwrap()],
+ )
+ .unwrap();
+ assert_eq!(
+ fs.read_file(&file, &mut tokio::io::sink())
+ .await
+ .unwrap_err()
+ .kind(),
+ ErrorKind::Corrupt
+ );
+ }
+}
+
+#[tokio::test]
+async fn verifies_unselected_blob_bytes_and_reports_destination_errors() {
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ let uploaded = fs.write_file(&mut &b"abcdef"[..]).await.unwrap();
+ let blob = uploaded.parts()[0].blob().clone();
+ let file = File::new(
+ ContentId::new(blake3::hash(b"bc").into(), 2),
+ vec![FilePart::new(0..2, 1, blob.clone()).unwrap()],
+ )
+ .unwrap();
+ let (mut sink, peer) = tokio::io::duplex(1);
+ drop(peer);
+ assert_eq!(
+ fs.read_file(&file, &mut sink).await.unwrap_err().kind(),
+ ErrorKind::Io
+ );
+ backend
+ .state
+ .lock()
+ .unwrap()
+ .objects
+ .get_mut(std::str::from_utf8(blob.as_bytes()).unwrap())
+ .unwrap()
+ .bytes[5] ^= 1;
+ assert_eq!(
+ fs.read_file(&file, &mut tokio::io::sink())
+ .await
+ .unwrap_err()
+ .kind(),
+ ErrorKind::Corrupt
+ );
+}
+
+#[tokio::test]
+async fn source_failure_after_upload_starts_aborts_the_writer() {
+ struct FailingSource(bool);
+ impl tokio::io::AsyncRead for FailingSource {
+ fn poll_read(
+ mut self: std::pin::Pin<&mut Self>,
+ _: &mut std::task::Context<'_>,
+ buffer: &mut tokio::io::ReadBuf<'_>,
+ ) -> std::task::Poll<std::io::Result<()>> {
+ if self.0 {
+ return std::task::Poll::Ready(Err(std::io::Error::other("source failed")));
+ }
+ self.0 = true;
+ buffer.put_slice(b"started");
+ std::task::Poll::Ready(Ok(()))
+ }
+ }
+ let backend = TestBackend::default();
+ let fs = Fs::create(backend.operator()).await.unwrap();
+ assert_eq!(
+ fs.write_file(&mut FailingSource(false))
+ .await
+ .unwrap_err()
+ .kind(),
+ ErrorKind::Io
+ );
+ assert_eq!(backend.state.lock().unwrap().data_aborts, 1);
+ assert_eq!(fs.observe().await.unwrap().version().number(), 0);
+}
+
+#[tokio::test]
+#[ignore = "requires an isolated S3 bucket configured with YINYANG_S3_* variables"]
+async fn s3_file_publication_and_reopen() {
+ opendal::install_default();
+ let config = std::env::vars()
+ .filter_map(|(key, value)| {
+ key.strip_prefix("YINYANG_S3_")
+ .map(|key| (key.to_ascii_lowercase(), value))
+ })
+ .collect::<Vec<_>>();
+ let operator = Operator::via_iter("s3", config).unwrap();
+ let fs = Fs::create(operator.clone()).await.unwrap();
+ let bytes = vec![37; 12 * 1024 * 1024 + 17];
+ let file = fs.write_file(&mut bytes.as_slice()).await.unwrap();
+ let observed = fs.observe().await.unwrap();
+ let name = format!("file-{}", uuid::Uuid::new_v4().simple());
+ let tree = add_file(observed.tree(), &name, file);
+ let id = CommitId::generate();
+ assert!(matches!(
+ fs.commit(&observed, id, tree.clone()).await.unwrap(),
+ CommitOutcome::Committed { .. }
+ ));
+ assert!(matches!(
+ fs.commit(&observed, id, tree).await.unwrap(),
+ CommitOutcome::Committed { .. }
+ ));
+ assert!(matches!(
+ fs.commit(
+ &observed,
+ CommitId::generate(),
+ add_directory(observed.tree(), "loser", NodeId::generate())
+ )
+ .await
+ .unwrap(),
+ CommitOutcome::Conflict { .. }
+ ));
+ let reopened = Fs::open(operator).await.unwrap();
+ let current = reopened.observe().await.unwrap();
+ let NodeBody::File(file) = current
+ .tree()
+ .get(&Path::new(name).unwrap())
+ .unwrap()
+ .body()
+ else {
+ panic!()
+ };
+ let mut output = Vec::new();
+ reopened.read_file(file, &mut output).await.unwrap();
+ assert_eq!(output, bytes);
+}
+
#[tokio::test]
async fn creates_and_reopens_one_filesystem() {
let backend = TestBackend::default();
@@ -647,7 +952,7 @@ async fn requires_node_generation_for_file_changes() {
node,
Generation::FIRST,
false,
- File::new(ContentId::new([1; 32], 0), Vec::new()).unwrap(),
+ filesystem.write_file(&mut &b"original"[..]).await.unwrap(),
),
);
filesystem
diff --git a/specs/yinyang-format.md b/specs/yinyang-format.md
index 9ac4a92..2aafc25 100644
--- a/specs/yinyang-format.md
+++ b/specs/yinyang-format.md
@@ -71,6 +71,7 @@ YinYang owns these paths below the operator root:
```text
.yinyang/head
.yinyang/versions/<object-id>
+.yinyang/data/<object-id>
```
`<object-id>` is a generated UUID encoded as 32 lowercase hexadecimal
@@ -85,6 +86,34 @@ condition mismatch is a publication conflict. Missing, truncated,
length-mismatched, or unverifiable referenced objects are corrupt data. Direct
external writes under `.yinyang/` are outside the format contract.
+## File content IO
+
+`Fs::write_file` consumes an asynchronous byte source and returns a `File`
+only after the backend acknowledges the immutable data object. It writes raw
+bytes at a fresh UUID key while computing BLAKE3 and length with bounded
+buffers. A non-empty file has one part spanning that object; an empty file
+has the BLAKE3 digest of empty bytes and no parts or object. A failed upload
+returns no file and attempts to abort the writer. Cancellation or an ambiguous
+close failure may leave unreachable data, never a published namespace entry.
+
+`Fs::read_file` streams into a caller-owned destination. Each part reads and
+verifies its entire source blob, including bytes outside the selected range;
+the assembled logical file digest is also verified. Repeated references may
+read the same blob again. Memory use does not grow with file size. Missing
+data, invalid data keys, length mismatches, and digest mismatches are `Corrupt`;
+other backend failures are `Storage`, and source/destination IO failures are
+`Io`. Bytes delivered before successful completion are provisional. Callers
+must discard them on failure and own destination flushing and durability.
+
+Before publishing, `Fs::commit` reads and verifies each new or changed file
+representation. Unchanged file bodies under the same node identity need no
+new read. This includes files constructed by callers rather than uploaded
+through `write_file`, and prevents publication of incomplete references. The
+initial implementation trades an extra read of new data for this guarantee.
+The guarantee assumes immutable data is not externally modified or deleted.
+Head conflicts can leave complete but unreachable uploads. There is no garbage
+collection yet; callers must not delete objects referenced by older versions.
+
## Persistent encoding
The payload uses [Borsh encoding](https://borsh.io/). Integers use little-endian