aboutsummaryrefslogtreecommitdiff
path: root/aero-dav
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
commitb9ce5886033677f6c65a4b873e17574fdb8df31d (patch)
tree9ed1d721361027d7d6fef0ecad65d7e1b74a7ddb /aero-dav
parent0dcf69f180f5a7b71b6ad2ac67e4cdd81e5154f1 (diff)
parent5954de6efbb040b8b47daf0c7663a60f3db1da6e (diff)
downloadaerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.tar.gz
aerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.zip
Merge branch 'caldav'
Diffstat (limited to 'aero-dav')
-rw-r--r--aero-dav/.gitignore1
-rw-r--r--aero-dav/Cargo.toml15
-rw-r--r--aero-dav/fuzz/.gitignore4
-rw-r--r--aero-dav/fuzz/Cargo.lock4249
-rw-r--r--aero-dav/fuzz/Cargo.toml24
-rw-r--r--aero-dav/fuzz/dav.dict126
-rw-r--r--aero-dav/fuzz/fuzz_targets/dav.rs209
-rw-r--r--aero-dav/src/acldecoder.rs84
-rw-r--r--aero-dav/src/aclencoder.rs71
-rw-r--r--aero-dav/src/acltypes.rs38
-rw-r--r--aero-dav/src/caldecoder.rs1421
-rw-r--r--aero-dav/src/calencoder.rs1036
-rw-r--r--aero-dav/src/caltypes.rs1500
-rw-r--r--aero-dav/src/decoder.rs1152
-rw-r--r--aero-dav/src/encoder.rs1262
-rw-r--r--aero-dav/src/error.rs62
-rw-r--r--aero-dav/src/lib.rs35
-rw-r--r--aero-dav/src/realization.rs260
-rw-r--r--aero-dav/src/syncdecoder.rs248
-rw-r--r--aero-dav/src/syncencoder.rs227
-rw-r--r--aero-dav/src/synctypes.rs86
-rw-r--r--aero-dav/src/types.rs964
-rw-r--r--aero-dav/src/versioningdecoder.rs132
-rw-r--r--aero-dav/src/versioningencoder.rs143
-rw-r--r--aero-dav/src/versioningtypes.rs59
-rw-r--r--aero-dav/src/xml.rs367
26 files changed, 13775 insertions, 0 deletions
diff --git a/aero-dav/.gitignore b/aero-dav/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/aero-dav/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/aero-dav/Cargo.toml b/aero-dav/Cargo.toml
new file mode 100644
index 0000000..c847f68
--- /dev/null
+++ b/aero-dav/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "aero-dav"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A partial and standalone implementation of the WebDAV protocol and its extensions (eg. CalDAV or CardDAV)"
+
+[dependencies]
+quick-xml.workspace = true
+http.workspace = true
+chrono.workspace = true
+tokio.workspace = true
+futures.workspace = true
+tracing.workspace = true
diff --git a/aero-dav/fuzz/.gitignore b/aero-dav/fuzz/.gitignore
new file mode 100644
index 0000000..1a45eee
--- /dev/null
+++ b/aero-dav/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/aero-dav/fuzz/Cargo.lock b/aero-dav/fuzz/Cargo.lock
new file mode 100644
index 0000000..08fa951
--- /dev/null
+++ b/aero-dav/fuzz/Cargo.lock
@@ -0,0 +1,4249 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "abnf-core"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea"
+dependencies = [
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aerogramme"
+version = "0.3.0"
+dependencies = [
+ "anyhow",
+ "argon2",
+ "async-trait",
+ "aws-config",
+ "aws-sdk-s3",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "backtrace",
+ "base64 0.21.7",
+ "chrono",
+ "clap",
+ "console-subscriber",
+ "duplexify",
+ "eml-codec",
+ "futures",
+ "hex",
+ "http 1.1.0",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
+ "im",
+ "imap-codec",
+ "imap-flow",
+ "itertools 0.10.5",
+ "k2v-client",
+ "lazy_static",
+ "ldap3",
+ "log",
+ "nix",
+ "nom 7.1.3",
+ "quick-xml",
+ "rand",
+ "rmp-serde",
+ "rpassword",
+ "rustls 0.22.2",
+ "rustls-pemfile 2.1.1",
+ "serde",
+ "smtp-message",
+ "smtp-server",
+ "sodiumoxide",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tokio-util",
+ "toml",
+ "tracing",
+ "tracing-subscriber",
+ "zstd",
+]
+
+[[package]]
+name = "aerogramme-fuzz"
+version = "0.0.0"
+dependencies = [
+ "aerogramme",
+ "libfuzzer-sys",
+ "quick-xml",
+ "tokio",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
+
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "asn1-rs"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 5.2.0",
+ "event-listener-strategy 0.5.0",
+ "futures-core",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
+dependencies = [
+ "async-lock 3.3.0",
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.0.1",
+ "futures-lite 2.2.0",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock 2.8.0",
+ "autocfg",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.2.0",
+ "async-executor",
+ "async-io 2.3.1",
+ "async-lock 3.3.0",
+ "blocking",
+ "futures-lite 2.2.0",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock 2.8.0",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite 1.13.0",
+ "log",
+ "parking",
+ "polling 2.8.0",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.10",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
+dependencies = [
+ "async-lock 3.3.0",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite 2.2.0",
+ "parking",
+ "polling 3.5.0",
+ "rustix 0.38.31",
+ "slab",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener 2.5.3",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
+dependencies = [
+ "event-listener 4.0.3",
+ "event-listener-strategy 0.4.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-net"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f"
+dependencies = [
+ "async-io 1.13.0",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "async-process"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88"
+dependencies = [
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "async-signal",
+ "blocking",
+ "cfg-if",
+ "event-listener 3.1.0",
+ "futures-lite 1.13.0",
+ "rustix 0.38.31",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
+dependencies = [
+ "async-io 2.3.1",
+ "async-lock 2.8.0",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 0.38.31",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-std"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite 1.13.0",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "auto_enums"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0dfe45d75158751e195799f47ea02e81f570aa24bc5ef999cdd9e888c4b5c3"
+dependencies = [
+ "auto_enums_core",
+ "auto_enums_derive",
+]
+
+[[package]]
+name = "auto_enums_core"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da47c46001293a2c4b744d731958be22cff408a2ab76e2279328f9713b1267b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "auto_enums_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41aed1da83ecdc799503b7cb94da1b45a34d72b49caf40a61d9cf5b88ec07cfd"
+dependencies = [
+ "autocfg",
+ "derive_utils",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "aws-config"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b96342ea8948ab9bef3e6234ea97fc32e2d8a88d8fb6a084e52267317f94b6b"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-sdk-sso",
+ "aws-sdk-ssooidc",
+ "aws-sdk-sts",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "hex",
+ "http 0.2.12",
+ "hyper 0.14.28",
+ "ring 0.17.8",
+ "time",
+ "tokio",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-credential-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "273fa47dafc9ef14c2c074ddddbea4561ff01b7f68d5091c0e9737ced605c01d"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-runtime"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e38bab716c8bf07da24be07ecc02e0f5656ce8f30a891322ecdcb202f943b85"
+dependencies = [
+ "aws-credential-types",
+ "aws-sigv4",
+ "aws-smithy-async",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "aws-sdk-config"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07979fd68679736ba306d6ea2a4dc2fd835ac4d454942c5d8920ef83ed2f979f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-s3"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d35d39379445970fc3e4ddf7559fff2c32935ce0b279f9cb27080d6b7c6d94"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-sigv4",
+ "aws-smithy-async",
+ "aws-smithy-checksums",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-smithy-xml",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "once_cell",
+ "percent-encoding",
+ "regex-lite",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "aws-sdk-sso"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84bd3925a17c9adbf6ec65d52104a44a09629d8f70290542beeee69a95aee7f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-ssooidc"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c2dae39e997f58bc4d6292e6244b26ba630c01ab671b6f9f44309de3eb80ab8"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-sts"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17fd9a53869fee17cea77e352084e1aa71e2c5e323d974c13a9c2bcfd9544c7f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-query",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-smithy-xml",
+ "aws-types",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sigv4"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ada00a4645d7d89f296fe0ddbc3fe3554f03035937c849a05d37ddffc1f29a1"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "crypto-bigint 0.5.5",
+ "form_urlencoded",
+ "hex",
+ "hmac",
+ "http 0.2.12",
+ "http 1.1.0",
+ "once_cell",
+ "p256",
+ "percent-encoding",
+ "ring 0.17.8",
+ "sha2",
+ "subtle",
+ "time",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-smithy-async"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf7f09a27286d84315dfb9346208abb3b0973a692454ae6d0bc8d803fcce3b4"
+dependencies = [
+ "futures-util",
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "aws-smithy-checksums"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fd4b66f2a8e7c84d7e97bda2666273d41d2a2e25302605bcf906b7b2661ae5e"
+dependencies = [
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "bytes",
+ "crc32c",
+ "crc32fast",
+ "hex",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "md-5",
+ "pin-project-lite 0.2.13",
+ "sha1",
+ "sha2",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-eventstream"
+version = "0.60.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
+dependencies = [
+ "aws-smithy-types",
+ "bytes",
+ "crc32fast",
+]
+
+[[package]]
+name = "aws-smithy-http"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6ca214a6a26f1b7ebd63aa8d4f5e2194095643023f9608edf99a58247b9d80d"
+dependencies = [
+ "aws-smithy-eventstream",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "bytes-utils",
+ "futures-core",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-json"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1af80ecf3057fb25fe38d1687e94c4601a7817c6a1e87c1b0635f7ecb644ace5"
+dependencies = [
+ "aws-smithy-types",
+]
+
+[[package]]
+name = "aws-smithy-query"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb27084f72ea5fc20033efe180618677ff4a2f474b53d84695cfe310a6526cbc"
+dependencies = [
+ "aws-smithy-types",
+ "urlencoding",
+]
+
+[[package]]
+name = "aws-smithy-runtime"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb5fca54a532a36ff927fbd7407a7c8eb9c3b4faf72792ba2965ea2cad8ed55"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
+ "once_cell",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "rustls 0.21.10",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-runtime-api"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22389cb6f7cac64f266fb9f137745a9349ced7b47e0d2ba503e9e40ede4f7060"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-types",
+ "bytes",
+ "http 0.2.12",
+ "http 1.1.0",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-smithy-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f081da5481210523d44ffd83d9f0740320050054006c719eae0232d411f024d3"
+dependencies = [
+ "base64-simd",
+ "bytes",
+ "bytes-utils",
+ "futures-core",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "itoa",
+ "num-integer",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "ryu",
+ "serde",
+ "time",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "aws-smithy-xml"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fccd8f595d0ca839f9f2548e66b99514a85f92feb4c01cf2868d93eb4888a42"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "aws-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07c63521aa1ea9a9f92a701f1a08ce3fd20b46c6efc0d5c8947c1fd879e3df1"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-async",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "http 0.2.12",
+ "rustc_version",
+ "tracing",
+]
+
+[[package]]
+name = "axum"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+
+[[package]]
+name = "bitmaps"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "bitvec"
+version = "0.19.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+dependencies = [
+ "async-channel 2.2.0",
+ "async-lock 3.3.0",
+ "async-task",
+ "fastrand 2.0.1",
+ "futures-io",
+ "futures-lite 2.2.0",
+ "piper",
+ "tracing",
+]
+
+[[package]]
+name = "bounded-static"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2325bd33fa7e3018e7e37f5b0591ba009124963b5a3f8b7cae6d0a8c1028ed4"
+dependencies = [
+ "bounded-static-derive",
+]
+
+[[package]]
+name = "bounded-static-derive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "bytes-utils"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
+dependencies = [
+ "bytes",
+ "either",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+dependencies = [
+ "jobserver",
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
+dependencies = [
+ "atty",
+ "bitflags 1.3.2",
+ "clap_derive",
+ "clap_lex",
+ "indexmap 1.9.3",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "console-api"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787"
+dependencies = [
+ "futures-core",
+ "prost",
+ "prost-types",
+ "tonic",
+ "tracing-core",
+]
+
+[[package]]
+name = "console-subscriber"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e"
+dependencies = [
+ "console-api",
+ "crossbeam-channel",
+ "crossbeam-utils",
+ "futures-task",
+ "hdrhistogram",
+ "humantime",
+ "prost-types",
+ "serde",
+ "serde_json",
+ "thread_local",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tracing",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32c"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89254598aa9b9fa608de44b3ae54c810f0f06d755e24c50177f1f8f31ff50ce2"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+
+[[package]]
+name = "der"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "der-parser"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_utils"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "duplexify"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1cc346cd6db38ceab2d33f59b26024c3ddb8e75f047c6cafbcbc016ea8065d5"
+dependencies = [
+ "async-std",
+ "pin-project-lite 0.1.12",
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.14.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
+dependencies = [
+ "der",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+]
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "signature",
+]
+
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
+dependencies = [
+ "base16ct",
+ "crypto-bigint 0.4.9",
+ "der",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "eml-codec"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4499124d87abce26a57ef96ece800fa8babc38fbedd81c607c340ae83d46d2e"
+dependencies = [
+ "base64 0.21.7",
+ "chrono",
+ "encoding_rs",
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
+dependencies = [
+ "event-listener 4.0.3",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
+dependencies = [
+ "event-listener 5.2.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "ff"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite 0.2.13",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-lite"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
+dependencies = [
+ "fastrand 2.0.1",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "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.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "group"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap 2.2.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 1.1.0",
+ "indexmap 2.2.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
+name = "hdrhistogram"
+version = "7.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
+dependencies = [
+ "base64 0.21.7",
+ "byteorder",
+ "flate2",
+ "nom 7.1.3",
+ "num-traits",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "socket2 0.5.6",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2 0.4.2",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
+dependencies = [
+ "futures-util",
+ "http 0.2.12",
+ "hyper 0.14.28",
+ "log",
+ "rustls 0.21.10",
+ "rustls-native-certs 0.6.3",
+ "tokio",
+ "tokio-rustls 0.24.1",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
+dependencies = [
+ "futures-util",
+ "http 1.1.0",
+ "hyper 1.2.0",
+ "hyper-util",
+ "log",
+ "rustls 0.22.2",
+ "rustls-native-certs 0.7.0",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper 0.14.28",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "hyper 1.2.0",
+ "pin-project-lite 0.2.13",
+ "socket2 0.5.6",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "im"
+version = "15.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9"
+dependencies = [
+ "bitmaps",
+ "rand_core",
+ "rand_xoshiro",
+ "sized-chunks",
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "imap-codec"
+version = "2.0.0"
+source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
+dependencies = [
+ "abnf-core",
+ "base64 0.21.7",
+ "bounded-static",
+ "chrono",
+ "imap-types",
+ "log",
+ "nom 7.1.3",
+ "thiserror",
+]
+
+[[package]]
+name = "imap-flow"
+version = "0.1.0"
+source = "git+https://github.com/duesee/imap-flow.git?branch=main#dce759a8531f317e8d7311fb032b366db6698e38"
+dependencies = [
+ "bounded-static",
+ "bytes",
+ "imap-codec",
+ "imap-types",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "imap-types"
+version = "2.0.0"
+source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
+dependencies = [
+ "base64 0.21.7",
+ "bounded-static",
+ "chrono",
+ "thiserror",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.3",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k2v-client"
+version = "0.0.4"
+source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?branch=k2v/shared_http_client#8b35a946d9f6b31b26b9783acbfab984316051f4"
+dependencies = [
+ "aws-sdk-config",
+ "aws-sigv4",
+ "base64 0.21.7",
+ "hex",
+ "http 1.1.0",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lber"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a99b520993b21a6faab32643cf4726573dc18ca4cf2d48cbeb24d248c86c930"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "nom 2.2.1",
+]
+
+[[package]]
+name = "ldap3"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce38dafca0608c64cc0146fb782b06abb8d946dae7a3af23c89a95da24f6b84d"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures",
+ "futures-util",
+ "lazy_static",
+ "lber",
+ "log",
+ "nom 2.2.1",
+ "percent-encoding",
+ "ring 0.16.20",
+ "rustls 0.20.9",
+ "rustls-native-certs 0.6.3",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.23.4",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+ "x509-parser",
+]
+
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "libsodium-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "walkdir",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.4.2",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
+
+[[package]]
+name = "nom"
+version = "6.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "oid-registry"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
+
+[[package]]
+name = "outref"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "p256"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "sha2",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.0.1",
+ "futures-io",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite 0.2.13",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "polling"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "pin-project-lite 0.2.13",
+ "rustix 0.38.31",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
+dependencies = [
+ "anyhow",
+ "itertools 0.11.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+ "tokio",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.6",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "rfc6979"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
+dependencies = [
+ "crypto-bigint 0.4.9",
+ "hmac",
+ "zeroize",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin 0.5.2",
+ "untrusted 0.7.1",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin 0.9.8",
+ "untrusted 0.9.0",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rmp"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
+dependencies = [
+ "byteorder",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723ecff9ad04f4ad92fe1c8ca6c20d2196d9286e9c60727c4cb5511629260e9d"
+dependencies = [
+ "byteorder",
+ "rmp",
+ "serde",
+]
+
+[[package]]
+name = "rpassword"
+version = "7.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
+dependencies = [
+ "libc",
+ "rtoolbox",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rtoolbox"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+dependencies = [
+ "bitflags 2.4.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.13",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
+dependencies = [
+ "log",
+ "ring 0.16.20",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
+dependencies = [
+ "log",
+ "ring 0.17.8",
+ "rustls-webpki 0.101.7",
+ "sct",
+]
+
+[[package]]
+name = "rustls"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41"
+dependencies = [
+ "log",
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 1.0.4",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 2.1.1",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
+dependencies = [
+ "base64 0.21.7",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
+dependencies = [
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[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.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "sec1"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "sized-chunks"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
+dependencies = [
+ "bitmaps",
+ "typenum",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
+
+[[package]]
+name = "smol"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-executor",
+ "async-fs",
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "smtp-message"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "auto_enums",
+ "futures",
+ "idna 0.2.3",
+ "lazy_static",
+ "nom 6.1.2",
+ "pin-project",
+ "regex-automata 0.1.10",
+ "serde",
+]
+
+[[package]]
+name = "smtp-server"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "duplexify",
+ "futures",
+ "smol",
+ "smtp-message",
+ "smtp-server-types",
+]
+
+[[package]]
+name = "smtp-server-types"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "serde",
+ "smtp-message",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "sodiumoxide"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028"
+dependencies = [
+ "ed25519",
+ "libc",
+ "libsodium-sys",
+ "serde",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "synstructure"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
+
+[[package]]
+name = "thiserror"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite 0.2.13",
+ "signal-hook-registry",
+ "socket2 0.5.6",
+ "tokio-macros",
+ "tracing",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
+dependencies = [
+ "rustls 0.20.9",
+ "tokio",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls 0.21.10",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
+dependencies = [
+ "rustls 0.22.2",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tonic"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.21.7",
+ "bytes",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "tokio",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite 0.2.13",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite 0.2.13",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[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.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.5.0",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "uuid"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "value-bag"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
+
+[[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.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.4",
+ "windows_aarch64_msvc 0.52.4",
+ "windows_i686_gnu 0.52.4",
+ "windows_i686_msvc 0.52.4",
+ "windows_x86_64_gnu 0.52.4",
+ "windows_x86_64_gnullvm 0.52.4",
+ "windows_x86_64_msvc 0.52.4",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "x509-parser"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c"
+dependencies = [
+ "asn1-rs",
+ "base64 0.13.1",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
+[[package]]
+name = "zstd"
+version = "0.9.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "4.1.3+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "1.6.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f"
+dependencies = [
+ "cc",
+ "libc",
+]
diff --git a/aero-dav/fuzz/Cargo.toml b/aero-dav/fuzz/Cargo.toml
new file mode 100644
index 0000000..a450853
--- /dev/null
+++ b/aero-dav/fuzz/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "aerogramme-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+arbitrary = { version = "1", optional = true, features = ["derive"] }
+libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
+tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
+quick-xml = { version = "0.31", features = ["async-tokio"] }
+
+[dependencies.aero-dav]
+path = ".."
+
+[[bin]]
+name = "dav"
+path = "fuzz_targets/dav.rs"
+test = false
+doc = false
+bench = false
diff --git a/aero-dav/fuzz/dav.dict b/aero-dav/fuzz/dav.dict
new file mode 100644
index 0000000..3ef5b69
--- /dev/null
+++ b/aero-dav/fuzz/dav.dict
@@ -0,0 +1,126 @@
+#
+# AFL dictionary for XML
+# ----------------------
+#
+# Several basic syntax elements and attributes, modeled on libxml2.
+#
+# Created by Michal Zalewski
+#
+
+attr_encoding=" encoding=\"1\""
+attr_generic=" a=\"1\""
+attr_href=" href=\"1\""
+attr_standalone=" standalone=\"no\""
+attr_version=" version=\"1\""
+attr_xml_base=" xml:base=\"1\""
+attr_xml_id=" xml:id=\"1\""
+attr_xml_lang=" xml:lang=\"1\""
+attr_xml_space=" xml:space=\"1\""
+attr_xmlns=" xmlns=\"1\""
+
+entity_builtin="&lt;"
+entity_decimal="&#1;"
+entity_external="&a;"
+entity_hex="&#x1;"
+
+string_any="ANY"
+string_brackets="[]"
+string_cdata="CDATA"
+string_col_fallback=":fallback"
+string_col_generic=":a"
+string_col_include=":include"
+string_dashes="--"
+string_empty="EMPTY"
+string_empty_dblquotes="\"\""
+string_empty_quotes="''"
+string_entities="ENTITIES"
+string_entity="ENTITY"
+string_fixed="#FIXED"
+string_id="ID"
+string_idref="IDREF"
+string_idrefs="IDREFS"
+string_implied="#IMPLIED"
+string_nmtoken="NMTOKEN"
+string_nmtokens="NMTOKENS"
+string_notation="NOTATION"
+string_parentheses="()"
+string_pcdata="#PCDATA"
+string_percent="%a"
+string_public="PUBLIC"
+string_required="#REQUIRED"
+string_schema=":schema"
+string_system="SYSTEM"
+string_ucs4="UCS-4"
+string_utf16="UTF-16"
+string_utf8="UTF-8"
+string_xmlns="xmlns:"
+
+tag_attlist="<!ATTLIST"
+tag_cdata="<![CDATA["
+tag_close="</a>"
+tag_doctype="<!DOCTYPE"
+tag_element="<!ELEMENT"
+tag_entity="<!ENTITY"
+tag_ignore="<![IGNORE["
+tag_include="<![INCLUDE["
+tag_notation="<!NOTATION"
+tag_open="<a>"
+tag_open_close="<a />"
+tag_open_exclamation="<!"
+tag_open_q="<?"
+tag_sq2_close="]]>"
+tag_xml_q="<?xml?>"
+
+"0"
+"1"
+"activelock"
+"allprop"
+"cannot-modify-protected-property"
+"collection"
+"creationdate"
+"DAV:"
+"depth"
+"displayname"
+"error"
+"exclusive"
+"getcontentlanguage"
+"getcontentlength"
+"getcontenttype"
+"getetag"
+"getlastmodified"
+"href"
+"include"
+"Infinite"
+"infinity"
+"location"
+"lockdiscovery"
+"lockentry"
+"lockinfo"
+"lockroot"
+"lockscope"
+"locktoken"
+"lock-token-matches-request-uri"
+"lock-token-submitted"
+"locktype"
+"multistatus"
+"no-conflicting-lock"
+"no-external-entities"
+"owner"
+"preserved-live-properties"
+"prop"
+"propertyupdate"
+"propfind"
+"propfind-finite-depth"
+"propname"
+"propstat"
+"remove"
+"resourcetype"
+"response"
+"responsedescription"
+"set"
+"shared"
+"status"
+"supportedlock"
+"text/html"
+"timeout"
+"write"
diff --git a/aero-dav/fuzz/fuzz_targets/dav.rs b/aero-dav/fuzz/fuzz_targets/dav.rs
new file mode 100644
index 0000000..a303401
--- /dev/null
+++ b/aero-dav/fuzz/fuzz_targets/dav.rs
@@ -0,0 +1,209 @@
+#![no_main]
+
+use libfuzzer_sys::arbitrary;
+use libfuzzer_sys::arbitrary::Arbitrary;
+use libfuzzer_sys::fuzz_target;
+
+use aero_dav::{realization, types, xml};
+use quick_xml::reader::NsReader;
+use tokio::io::AsyncWriteExt;
+use tokio::runtime::Runtime;
+
+// Split this file
+const tokens: [&str; 63] = [
+ "0",
+ "1",
+ "activelock",
+ "allprop",
+ "encoding",
+ "utf-8",
+ "http://ns.example.com/boxschema/",
+ "HTTP/1.1 200 OK",
+ "1997-12-01T18:27:21-08:00",
+ "Mon, 12 Jan 1998 09:25:56 GMT",
+ "\"abcdef\"",
+ "cannot-modify-protected-property",
+ "collection",
+ "creationdate",
+ "DAV:",
+ "D",
+ "C",
+ "xmlns:D",
+ "depth",
+ "displayname",
+ "error",
+ "exclusive",
+ "getcontentlanguage",
+ "getcontentlength",
+ "getcontenttype",
+ "getetag",
+ "getlastmodified",
+ "href",
+ "include",
+ "Infinite",
+ "infinity",
+ "location",
+ "lockdiscovery",
+ "lockentry",
+ "lockinfo",
+ "lockroot",
+ "lockscope",
+ "locktoken",
+ "lock-token-matches-request-uri",
+ "lock-token-submitted",
+ "locktype",
+ "multistatus",
+ "no-conflicting-lock",
+ "no-external-entities",
+ "owner",
+ "preserved-live-properties",
+ "prop",
+ "propertyupdate",
+ "propfind",
+ "propfind-finite-depth",
+ "propname",
+ "propstat",
+ "remove",
+ "resourcetype",
+ "response",
+ "responsedescription",
+ "set",
+ "shared",
+ "status",
+ "supportedlock",
+ "text/html",
+ "timeout",
+ "write",
+];
+
+#[derive(Arbitrary)]
+enum Token {
+ Known(usize),
+ //Unknown(String),
+}
+impl Token {
+ fn serialize(&self) -> String {
+ match self {
+ Self::Known(i) => tokens[i % tokens.len()].to_string(),
+ //Self::Unknown(v) => v.to_string(),
+ }
+ }
+}
+
+#[derive(Arbitrary)]
+struct Tag {
+ //prefix: Option<Token>,
+ name: Token,
+ attr: Option<(Token, Token)>,
+}
+impl Tag {
+ fn start(&self) -> String {
+ let mut acc = String::new();
+ /*if let Some(p) = &self.prefix {
+ acc.push_str(p.serialize().as_str());
+ acc.push_str(":");
+ }*/
+ acc.push_str("D:");
+ acc.push_str(self.name.serialize().as_str());
+
+ if let Some((k, v)) = &self.attr {
+ acc.push_str(" ");
+ acc.push_str(k.serialize().as_str());
+ acc.push_str("=\"");
+ acc.push_str(v.serialize().as_str());
+ acc.push_str("\"");
+ }
+ acc
+ }
+ fn end(&self) -> String {
+ let mut acc = String::new();
+ acc.push_str("D:");
+ acc.push_str(self.name.serialize().as_str());
+ acc
+ }
+}
+
+#[derive(Arbitrary)]
+enum XmlNode {
+ //@FIXME: build RFC3339 and RFC822 Dates with chrono based on timestamps
+ //@FIXME: add small numbers
+ //@FIXME: add http status code
+ Node(Tag, Vec<Self>),
+ Number(u64),
+ Text(Token),
+}
+impl std::fmt::Debug for XmlNode {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.serialize())
+ }
+}
+impl XmlNode {
+ fn serialize(&self) -> String {
+ match self {
+ Self::Node(tag, children) => {
+ let stag = tag.start();
+ match children.is_empty() {
+ true => format!("<{}/>", stag),
+ false => format!(
+ "<{}>{}</{}>",
+ stag,
+ children.iter().map(|v| v.serialize()).collect::<String>(),
+ tag.end()
+ ),
+ }
+ }
+ Self::Number(v) => format!("{}", v),
+ Self::Text(v) => v.serialize(),
+ }
+ }
+}
+
+async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![("xmlns:D".into(), "DAV:".into())];
+ let mut writer = xml::Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+
+ return buffer;
+}
+
+type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
+
+fuzz_target!(|nodes: XmlNode| {
+ let gen = format!(
+ "<D:multistatus xmlns:D=\"DAV:\">{}<D:/multistatus>",
+ nodes.serialize()
+ );
+ //println!("--------\n{}", gen);
+ let data = gen.as_bytes();
+
+ let rt = Runtime::new().expect("tokio runtime initialization");
+
+ rt.block_on(async {
+ // 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
+ let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
+ Err(_) => return,
+ Ok(r) => r,
+ };
+ let reference = match rdr.find::<Object>().await {
+ Err(_) => return,
+ Ok(m) => m,
+ };
+
+ // 2. Re-serialize the input
+ let my_serialization = serialize(&reference).await;
+
+ // 3. De-serialize my serialization
+ let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice()))
+ .await
+ .expect("XML Reader init");
+ let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
+
+ // 4. Both the first decoding and last decoding must be identical
+ assert_eq!(reference, comparison);
+ })
+});
diff --git a/aero-dav/src/acldecoder.rs b/aero-dav/src/acldecoder.rs
new file mode 100644
index 0000000..405286e
--- /dev/null
+++ b/aero-dav/src/acldecoder.rs
@@ -0,0 +1,84 @@
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::Owner(href));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ let user = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal(user));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet(vec![]));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "owner").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Owner);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet);
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "principal").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Principal);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// -----
+impl QRead<User> for User {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Unauthenticated);
+ }
+
+ dav::Href::qread(xml).await.map(Self::Authenticated)
+ }
+}
diff --git a/aero-dav/src/aclencoder.rs b/aero-dav/src/aclencoder.rs
new file mode 100644
index 0000000..28c01a7
--- /dev/null
+++ b/aero-dav/src/aclencoder.rs
@@ -0,0 +1,71 @@
+use quick_xml::events::Event;
+use quick_xml::Error as QError;
+
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Owner(href) => {
+ let start = xml.create_dav_element("owner");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CurrentUserPrincipal(user) => {
+ let start = xml.create_dav_element("current-user-principal");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ user.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CurrentUserPrivilegeSet(_) => {
+ let empty_tag = xml.create_dav_element("current-user-privilege-set");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Self::Owner => atom("owner").await,
+ Self::CurrentUserPrincipal => atom("current-user-principal").await,
+ Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await,
+ }
+ }
+}
+
+impl QWrite for ResourceType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Principal => {
+ let empty_tag = xml.create_dav_element("principal");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+// -----
+
+impl QWrite for User {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Unauthenticated => {
+ let tag = xml.create_dav_element("unauthenticated");
+ xml.q.write_event_async(Event::Empty(tag)).await
+ }
+ Self::Authenticated(href) => href.qwrite(xml).await,
+ }
+ }
+}
diff --git a/aero-dav/src/acltypes.rs b/aero-dav/src/acltypes.rs
new file mode 100644
index 0000000..0af3c8a
--- /dev/null
+++ b/aero-dav/src/acltypes.rs
@@ -0,0 +1,38 @@
+use super::types as dav;
+
+//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension)
+
+//@FIXME required for a full CalDAV implementation
+// See section 6. of the CalDAV RFC
+// It seems mainly required for free-busy that I will not implement now.
+// It can also be used for discovering main calendar, not sure it is used.
+// Note: it is used by Thunderbird
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Owner,
+ CurrentUserPrincipal,
+ CurrentUserPrivilegeSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ Owner(dav::Href),
+ CurrentUserPrincipal(User),
+ CurrentUserPrivilegeSet(Vec<Privilege>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Principal,
+}
+
+/// Not implemented, it's a placeholder
+#[derive(Debug, PartialEq, Clone)]
+pub struct Privilege(());
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum User {
+ Unauthenticated,
+ Authenticated(dav::Href),
+}
diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs
new file mode 100644
index 0000000..9ed783a
--- /dev/null
+++ b/aero-dav/src/caldecoder.rs
@@ -0,0 +1,1421 @@
+use chrono::NaiveDateTime;
+use quick_xml::events::Event;
+
+use super::caltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, CAL_URN, DAV_URN};
+
+// ---- ROOT ELEMENTS ---
+impl<E: dav::Extension> QRead<MkCalendar<E>> for MkCalendar<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar").await?;
+ let set = xml.find().await?;
+ xml.close().await?;
+ Ok(MkCalendar(set))
+ }
+}
+
+impl<E: dav::Extension> QRead<MkCalendarResponse<E>> for MkCalendarResponse<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar-response").await?;
+ let propstats = xml.collect().await?;
+ xml.close().await?;
+ Ok(MkCalendarResponse(propstats))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match CalendarQuery::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Query),
+ }
+
+ match CalendarMultiget::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Multiget),
+ }
+
+ FreeBusyQuery::qread(xml).await.map(Self::FreeBusy)
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarQuery<E>> for CalendarQuery<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-query").await?;
+ let (mut selector, mut filter, mut timezone) = (None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_read(&mut filter, &mut dirty).await?;
+ xml.maybe_read(&mut timezone, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+
+ match filter {
+ Some(filter) => Ok(CalendarQuery {
+ selector,
+ filter,
+ timezone,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarMultiget<E>> for CalendarMultiget<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-multiget").await?;
+ let mut selector = None;
+ let mut href = Vec::new();
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_push(&mut href, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(CalendarMultiget { selector, href })
+ }
+}
+
+impl QRead<FreeBusyQuery> for FreeBusyQuery {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "free-busy-query").await?;
+ let range = xml.find().await?;
+ xml.close().await?;
+ Ok(FreeBusyQuery(range))
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "calendar-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Query);
+ }
+ if xml
+ .maybe_open(DAV_URN, "calendar-multiget")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::Multiget);
+ }
+ if xml.maybe_open(DAV_URN, "free-busy-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::FreeBusy);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- EXTENSIONS ---
+impl QRead<Violation> for Violation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "resource-must-be-null")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ResourceMustBeNull)
+ } else if xml.maybe_open(DAV_URN, "need-privileges").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::NeedPrivileges)
+ } else if xml
+ .maybe_open(CAL_URN, "calendar-collection-location-ok")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::CalendarCollectionLocationOk)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "initialize-calendar-collection")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::InitializeCalendarCollection)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-object-resource")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarObjectResource)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-component")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarComponent)
+ } else if xml.maybe_open(CAL_URN, "no-uid-conflict").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ Ok(Self::NoUidConflict(href))
+ } else if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxResourceSize)
+ } else if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MinDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxInstances)
+ } else if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxAttendeesPerInstance)
+ } else if xml.maybe_open(CAL_URN, "valid-filter").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::ValidFilter)
+ } else if xml.maybe_open(CAL_URN, "supported-filter").await?.is_some() {
+ let (mut comp, mut prop, mut param) = (Vec::new(), Vec::new(), Vec::new());
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+ xml.maybe_push(&mut param, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+ Ok(Self::SupportedFilter { comp, prop, param })
+ } else if xml
+ .maybe_open(CAL_URN, "number-of-matches-within-limits")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::NumberOfMatchesWithinLimits)
+ } else {
+ Err(ParsingError::Recoverable)
+ }
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarHomeSet(href));
+ }
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ let lang = xml.prev_attr("xml:lang");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarDescription { lang, text });
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ let tz = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarTimezone(tz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ let comp = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarComponentSet(comp));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ let mime = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarData(mime));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxResourceSize(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-date-time")
+ .await?
+ .is_some()
+ {
+ let dtstr = xml.tag_string().await?;
+ let dt = NaiveDateTime::parse_from_str(dtstr.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ xml.close().await?;
+ return Ok(Property::MaxDateTime(dt));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-instances")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxInstances(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxAttendeesPerInstance(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ let cols = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCollationSet(cols));
+ }
+
+ let mut dirty = false;
+ let mut caldata: Option<CalendarDataPayload> = None;
+ xml.maybe_read(&mut caldata, &mut dirty).await?;
+ if let Some(cal) = caldata {
+ return Ok(Property::CalendarData(cal));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarHomeSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarDescription);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarTimezone);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarComponentSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarData);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxResourceSize);
+ }
+ if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MinDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxInstances);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxAttendeesPerInstance);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCollationSet);
+ }
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr
+ .ok_or(ParsingError::Recoverable)
+ .map(Self::CalendarData)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "calendar").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Calendar);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- INNER XML ----
+impl QRead<SupportedCollation> for SupportedCollation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "supported-collation").await?;
+ let col = Collation::new(xml.tag_string().await?);
+ xml.close().await?;
+ Ok(SupportedCollation(col))
+ }
+}
+
+impl QRead<CalendarDataPayload> for CalendarDataPayload {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let payload = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(CalendarDataPayload { mime, payload })
+ }
+}
+
+impl QRead<CalendarDataSupport> for CalendarDataSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let ct = xml.prev_attr("content-type");
+ let vs = xml.prev_attr("version");
+ match (ct, vs) {
+ (Some(content_type), Some(version)) => Ok(Self {
+ content_type,
+ version,
+ }),
+ _ => Err(ParsingError::Recoverable),
+ }
+ }
+}
+
+impl QRead<CalendarDataRequest> for CalendarDataRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let (mut comp, mut recurrence, mut limit_freebusy_set) = (None, None, None);
+
+ if !xml.parent_has_child() {
+ return Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut comp, &mut dirty).await?;
+ xml.maybe_read(&mut recurrence, &mut dirty).await?;
+ xml.maybe_read(&mut limit_freebusy_set, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ })
+ }
+}
+
+impl QRead<CalendarDataEmpty> for CalendarDataEmpty {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ xml.close().await?;
+ Ok(Self(mime))
+ }
+}
+
+impl QRead<Comp> for Comp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let (mut prop_kind, mut comp_kind) = (None, None);
+
+ let bs = xml.open(CAL_URN, "comp").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+
+ // Return early if it's an empty tag
+ if matches!(bs, Event::Empty(_)) {
+ xml.close().await?;
+ return Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ let (mut tmp_prop_kind, mut tmp_comp_kind): (Option<PropKind>, Option<CompKind>) =
+ (None, None);
+
+ xml.maybe_read(&mut tmp_prop_kind, &mut dirty).await?;
+ Box::pin(xml.maybe_read(&mut tmp_comp_kind, &mut dirty)).await?;
+
+ //@FIXME hack
+ // Merge
+ match (tmp_prop_kind, &mut prop_kind) {
+ (Some(PropKind::Prop(mut a)), Some(PropKind::Prop(ref mut b))) => b.append(&mut a),
+ (Some(PropKind::AllProp), v) => *v = Some(PropKind::AllProp),
+ (Some(x), b) => *b = Some(x),
+ (None, _) => (),
+ };
+ match (tmp_comp_kind, &mut comp_kind) {
+ (Some(CompKind::Comp(mut a)), Some(CompKind::Comp(ref mut b))) => b.append(&mut a),
+ (Some(CompKind::AllComp), v) => *v = Some(CompKind::AllComp),
+ (Some(a), b) => *b = Some(a),
+ (None, _) => (),
+ };
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ })
+ }
+}
+
+impl QRead<CompSupport> for CompSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp").await?;
+ let inner = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<CompKind> for CompKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut comp = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allcomp").await?.is_some() {
+ xml.close().await?;
+ return Ok(CompKind::AllComp);
+ }
+
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+ match &comp[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(CompKind::Comp(comp)),
+ }
+ }
+}
+
+impl QRead<PropKind> for PropKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut prop = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allprop").await?.is_some() {
+ xml.close().await?;
+ return Ok(PropKind::AllProp);
+ }
+
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+
+ match &prop[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(PropKind::Prop(prop)),
+ }
+ }
+}
+
+impl QRead<RecurrenceModifier> for RecurrenceModifier {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Expand::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(RecurrenceModifier::Expand),
+ }
+ LimitRecurrenceSet::qread(xml)
+ .await
+ .map(RecurrenceModifier::LimitRecurrenceSet)
+ }
+}
+
+impl QRead<Expand> for Expand {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "expand").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(Expand(start, end))
+ }
+}
+
+impl QRead<LimitRecurrenceSet> for LimitRecurrenceSet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-recurrence-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitRecurrenceSet(start, end))
+ }
+}
+
+impl QRead<LimitFreebusySet> for LimitFreebusySet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-freebusy-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitFreebusySet(start, end))
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarSelector<E>> for CalendarSelector<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ // allprop
+ if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
+ xml.close().await?;
+ return Ok(Self::AllProp);
+ }
+
+ // propname
+ if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
+ xml.close().await?;
+ return Ok(Self::PropName);
+ }
+
+ // prop
+ let (mut maybe_prop, mut dirty) = (None, false);
+ xml.maybe_read::<dav::PropName<E>>(&mut maybe_prop, &mut dirty)
+ .await?;
+ if let Some(prop) = maybe_prop {
+ return Ok(Self::Prop(prop));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<CompFilter> for CompFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp-filter").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = Box::pin(xml.maybe_find()).await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<CompFilterRules> for CompFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_range = None;
+ let mut prop_filter = Vec::new();
+ let mut comp_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_range, &mut dirty).await?;
+ xml.maybe_push(&mut prop_filter, &mut dirty).await?;
+ xml.maybe_push(&mut comp_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_range, &prop_filter[..], &comp_filter[..]) {
+ (None, [], []) => Err(ParsingError::Recoverable),
+ _ => Ok(Self::Matches(CompFilterMatch {
+ time_range,
+ prop_filter,
+ comp_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<CompFilterMatch> for CompFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<PropFilter> for PropFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop-filter").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<PropFilterRules> for PropFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_or_text = None;
+ let mut param_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_or_text, &mut dirty).await?;
+ xml.maybe_push(&mut param_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_or_text, &param_filter[..]) {
+ (None, []) => Err(ParsingError::Recoverable),
+ _ => Ok(PropFilterRules::Match(PropFilterMatch {
+ time_or_text,
+ param_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<PropFilterMatch> for PropFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<ParamFilter> for ParamFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "param-filter").await?;
+ let name = PropertyParameter(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<TimeOrText> for TimeOrText {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match TimeRange::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Time),
+ }
+ TextMatch::qread(xml).await.map(Self::Text)
+ }
+}
+
+impl QRead<TextMatch> for TextMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "text-match").await?;
+ let collation = xml.prev_attr("collation").map(Collation::new);
+ let negate_condition = xml.prev_attr("negate-condition").map(|v| v == "yes");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self {
+ collation,
+ negate_condition,
+ text,
+ })
+ }
+}
+
+impl QRead<ParamFilterMatch> for ParamFilterMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+ TextMatch::qread(xml).await.map(Self::Match)
+ }
+}
+
+impl QRead<TimeZone> for TimeZone {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "timezone").await?;
+ let inner = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<Filter> for Filter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "filter").await?;
+ let comp_filter = xml.find().await?;
+ xml.close().await?;
+ Ok(Self(comp_filter))
+ }
+}
+
+impl QRead<TimeRange> for TimeRange {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "time-range").await?;
+
+ let start = match xml.prev_attr("start") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+ let end = match xml.prev_attr("end") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+
+ xml.close().await?;
+
+ match (start, end) {
+ (Some(start), Some(end)) => {
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+ Ok(TimeRange::FullRange(start, end))
+ }
+ (Some(start), None) => Ok(TimeRange::OnlyStart(start)),
+ (None, Some(end)) => Ok(TimeRange::OnlyEnd(end)),
+ (None, None) => Err(ParsingError::MissingAttribute),
+ }
+ }
+}
+
+impl QRead<CalProp> for CalProp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let novalue = xml.prev_attr("novalue").map(|v| v == "yes");
+ xml.close().await?;
+ Ok(Self { name, novalue })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::xml::Node;
+ use chrono::{TimeZone, Utc};
+ //use quick_reader::NsReader;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn simple_comp_filter() {
+ let expected = CompFilter {
+ name: Component::VEvent,
+ additional_rules: None,
+ };
+ let src = r#"<C:comp-filter name="VEVENT" xmlns:C="urn:ietf:params:xml:ns:caldav" />"#;
+ let got = deserialize::<CompFilter>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn basic_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![dav::Property::DisplayName(
+ "Lisa's Events".into(),
+ )])));
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>
+"#;
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![
+ dav::Property::DisplayName("Lisa's Events".into()),
+ dav::Property::Extension(Property::CalendarDescription {
+ lang: Some("en".into()),
+ text: "Calendar restricted to events.".into(),
+ }),
+ dav::Property::Extension(Property::SupportedCalendarComponentSet(vec![
+ CompSupport(Component::VEvent)
+ ])),
+ dav::Property::Extension(Property::CalendarTimezone("BEGIN:VCALENDAR\nPRODID:-//Example Corp.//CalDAV Client//EN\nVERSION:2.0\nEND:VCALENDAR".into())),
+ ])));
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ <C:calendar-description xml:lang="en"
+ >Calendar restricted to events.</C:calendar-description>
+ <C:supported-calendar-component-set>
+ <C:comp name="VEVENT"/>
+ </C:supported-calendar-component-set>
+ <C:calendar-timezone><![CDATA[BEGIN:VCALENDAR
+PRODID:-//Example Corp.//CalDAV Client//EN
+VERSION:2.0
+END:VCALENDAR]]></C:calendar-timezone>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>"#;
+
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query() {
+ let expected = CalendarQuery {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: Some(Comp {
+ name: Component::VCalendar,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(PropKind::Prop(vec![
+ CalProp {
+ name: ComponentProperty("SUMMARY".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("UID".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTSTART".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTEND".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DURATION".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RECURRENCE-ID".into()),
+ novalue: None,
+ },
+ ])),
+ comp_kind: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: None,
+ },
+ ])),
+ }),
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 4, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data>
+ <C:comp name="VCALENDAR">
+ <C:prop name="VERSION"/>
+ <C:comp name="VEVENT">
+ <C:prop name="SUMMARY"/>
+ <C:prop name="UID"/>
+ <C:prop name="DTSTART"/>
+ <C:prop name="DTEND"/>
+ <C:prop name="DURATION"/>
+ <C:prop name="RRULE"/>
+ <C:prop name="RDATE"/>
+ <C:prop name="EXRULE"/>
+ <C:prop name="EXDATE"/>
+ <C:prop name="RECURRENCE-ID"/>
+ </C:comp>
+ <C:comp name="VTIMEZONE"/>
+ </C:comp>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060104T000000Z"
+ end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+</C:calendar-query>
+"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query_res() {
+ let expected = dav::Multistatus::<Calendar> {
+ extension: None,
+ responses: vec![
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ },
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let src = r#"<D:multistatus xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd2"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd3"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+"#;
+
+ let got = deserialize::<dav::Multistatus<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_recurring_evt() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: None,
+ recurrence: Some(RecurrenceModifier::LimitRecurrenceSet(
+ LimitRecurrenceSet(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ ),
+ )),
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <C:calendar-data>
+ <C:limit-recurrence-set start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_pending_todos() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: None,
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VTodo,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ comp_filter: vec![],
+ prop_filter: vec![
+ PropFilter {
+ name: ComponentProperty("COMPLETED".into()),
+ additional_rules: Some(PropFilterRules::IsNotDefined),
+ },
+ PropFilter {
+ name: ComponentProperty("STATUS".into()),
+ additional_rules: Some(PropFilterRules::Match(
+ PropFilterMatch {
+ param_filter: vec![],
+ time_or_text: Some(TimeOrText::Text(TextMatch {
+ collation: None,
+ negate_condition: Some(true),
+ text: "CANCELLED".into(),
+ })),
+ },
+ )),
+ },
+ ],
+ })),
+ }],
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:prop-filter name="COMPLETED">
+ <C:is-not-defined/>
+ </C:prop-filter>
+ <C:prop-filter name="STATUS">
+ <C:text-match
+ negate-condition="yes">CANCELLED</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+}
diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs
new file mode 100644
index 0000000..15df965
--- /dev/null
+++ b/aero-dav/src/calencoder.rs
@@ -0,0 +1,1036 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::caltypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, Node, QWrite, Writer};
+
+// ==================== Calendar Types Serialization =========================
+
+// -------------------- MKCALENDAR METHOD ------------------------------------
+impl<E: Extension> QWrite for MkCalendar<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("mkcalendar");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for MkCalendarResponse<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("mkcalendar-response");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propstat in self.0.iter() {
+ propstat.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// ----------------------- REPORT METHOD -------------------------------------
+impl QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query => {
+ let start = xml.create_dav_element("calendar-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::Multiget => {
+ let start = xml.create_dav_element("calendar-multiget");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::FreeBusy => {
+ let start = xml.create_dav_element("free-busy-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for ReportType<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query(v) => v.qwrite(xml).await,
+ Self::Multiget(v) => v.qwrite(xml).await,
+ Self::FreeBusy(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for CalendarQuery<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("calendar-query");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(selector) = &self.selector {
+ selector.qwrite(xml).await?;
+ }
+ self.filter.qwrite(xml).await?;
+ if let Some(tz) = &self.timezone {
+ tz.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for CalendarMultiget<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("calendar-multiget");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(selector) = &self.selector {
+ selector.qwrite(xml).await?;
+ }
+ for href in self.href.iter() {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for FreeBusyQuery {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("free-busy-query");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// -------------------------- DAV::prop --------------------------------------
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_cal_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Self::CalendarHomeSet => atom("calendar-home-set").await,
+ Self::CalendarDescription => atom("calendar-description").await,
+ Self::CalendarTimezone => atom("calendar-timezone").await,
+ Self::SupportedCalendarComponentSet => atom("supported-calendar-component-set").await,
+ Self::SupportedCalendarData => atom("supported-calendar-data").await,
+ Self::MaxResourceSize => atom("max-resource-size").await,
+ Self::MinDateTime => atom("min-date-time").await,
+ Self::MaxDateTime => atom("max-date-time").await,
+ Self::MaxInstances => atom("max-instances").await,
+ Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
+ Self::SupportedCollationSet => atom("supported-collation-set").await,
+ Self::CalendarData(req) => req.qwrite(xml).await,
+ }
+ }
+}
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::CalendarHomeSet(href) => {
+ let start = xml.create_cal_element("calendar-home-set");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarDescription { lang, text } => {
+ let mut start = xml.create_cal_element("calendar-description");
+ if let Some(the_lang) = lang {
+ start.push_attribute(("xml:lang", the_lang.as_str()));
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(text)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarTimezone(payload) => {
+ let start = xml.create_cal_element("calendar-timezone");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(payload)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCalendarComponentSet(many_comp) => {
+ let start = xml.create_cal_element("supported-calendar-component-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for comp in many_comp.iter() {
+ comp.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCalendarData(many_mime) => {
+ let start = xml.create_cal_element("supported-calendar-data");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for mime in many_mime.iter() {
+ mime.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxResourceSize(bytes) => {
+ let start = xml.create_cal_element("max-resource-size");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(bytes.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MinDateTime(dt) => {
+ let start = xml.create_cal_element("min-date-time");
+ let end = start.to_end();
+
+ let dtstr = format!("{}", dt.format(UTC_DATETIME_FMT));
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(dtstr.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxDateTime(dt) => {
+ let start = xml.create_cal_element("max-date-time");
+ let end = start.to_end();
+
+ let dtstr = format!("{}", dt.format(UTC_DATETIME_FMT));
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(dtstr.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxInstances(count) => {
+ let start = xml.create_cal_element("max-instances");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(count.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxAttendeesPerInstance(count) => {
+ let start = xml.create_cal_element("max-attendees-per-instance");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(count.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCollationSet(many_collations) => {
+ let start = xml.create_cal_element("supported-collation-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for collation in many_collations.iter() {
+ collation.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarData(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+// ---------------------- DAV::resourcetype ----------------------------------
+impl QWrite for ResourceType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Calendar => {
+ let empty_tag = xml.create_cal_element("calendar");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+// --------------------------- DAV::error ------------------------------------
+impl QWrite for Violation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_cal_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ //@FIXME
+ // DAV elements, should not be here but in RFC3744 on ACLs
+ // (we do not use atom as this error is in the DAV namespace, not the caldav one)
+ Self::NeedPrivileges => {
+ let empty_tag = xml.create_dav_element("need-privileges");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+
+ // Regular CalDAV errors
+ Self::ResourceMustBeNull => atom("resource-must-be-null").await,
+ Self::CalendarCollectionLocationOk => atom("calendar-collection-location-ok").await,
+ Self::ValidCalendarData => atom("valid-calendar-data").await,
+ Self::InitializeCalendarCollection => atom("initialize-calendar-collection").await,
+ Self::SupportedCalendarData => atom("supported-calendar-data").await,
+ Self::ValidCalendarObjectResource => atom("valid-calendar-object-resource").await,
+ Self::SupportedCalendarComponent => atom("supported-calendar-component").await,
+ Self::NoUidConflict(href) => {
+ let start = xml.create_cal_element("no-uid-conflict");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxResourceSize => atom("max-resource-size").await,
+ Self::MinDateTime => atom("min-date-time").await,
+ Self::MaxDateTime => atom("max-date-time").await,
+ Self::MaxInstances => atom("max-instances").await,
+ Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
+ Self::ValidFilter => atom("valid-filter").await,
+ Self::SupportedFilter { comp, prop, param } => {
+ let start = xml.create_cal_element("supported-filter");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for comp_item in comp.iter() {
+ comp_item.qwrite(xml).await?;
+ }
+ for prop_item in prop.iter() {
+ prop_item.qwrite(xml).await?;
+ }
+ for param_item in param.iter() {
+ param_item.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::NumberOfMatchesWithinLimits => atom("number-of-matches-within-limits").await,
+ }
+ }
+}
+
+// ---------------------------- Inner XML ------------------------------------
+impl QWrite for SupportedCollation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("supported-collation");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Collation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let col = match self {
+ Self::AsciiCaseMap => "i;ascii-casemap",
+ Self::Octet => "i;octet",
+ Self::Unknown(v) => v.as_str(),
+ };
+
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(col)))
+ .await
+ }
+}
+
+impl QWrite for CalendarDataSupport {
+ async fn qwrite(&self, _xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ unreachable!();
+ }
+}
+
+impl QWrite for CalendarDataPayload {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.mime {
+ start.push_attribute(("content-type", mime.content_type.as_str()));
+ start.push_attribute(("version", mime.version.as_str()));
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.payload.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for CalendarDataRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.mime {
+ start.push_attribute(("content-type", mime.content_type.as_str()));
+ start.push_attribute(("version", mime.version.as_str()));
+ }
+
+ // Empty tag
+ if self.comp.is_none() && self.recurrence.is_none() && self.limit_freebusy_set.is_none() {
+ return xml.q.write_event_async(Event::Empty(start.clone())).await;
+ }
+
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(comp) = &self.comp {
+ comp.qwrite(xml).await?;
+ }
+ if let Some(recurrence) = &self.recurrence {
+ recurrence.qwrite(xml).await?;
+ }
+ if let Some(freebusy) = &self.limit_freebusy_set {
+ freebusy.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for CalendarDataEmpty {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.0 {
+ empty.push_attribute(("content-type", mime.content_type.as_str()));
+ empty.push_attribute(("version", mime.version.as_str()));
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for Comp {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("comp");
+ start.push_attribute(("name", self.name.as_str()));
+ match (&self.prop_kind, &self.comp_kind) {
+ (None, None) => xml.q.write_event_async(Event::Empty(start)).await,
+ _ => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(prop_kind) = &self.prop_kind {
+ prop_kind.qwrite(xml).await?;
+ }
+ if let Some(comp_kind) = &self.comp_kind {
+ comp_kind.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for CompSupport {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("comp");
+ empty.push_attribute(("name", self.0.as_str()));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for CompKind {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllComp => {
+ let empty_tag = xml.create_cal_element("allcomp");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Comp(many_comp) => {
+ for comp in many_comp.iter() {
+ // Required: recursion in an async fn requires boxing
+ // rustc --explain E0733
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a Comp,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp, xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for PropKind {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllProp => {
+ let empty_tag = xml.create_cal_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Prop(many_prop) => {
+ for prop in many_prop.iter() {
+ prop.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for CalProp {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("prop");
+ empty.push_attribute(("name", self.name.0.as_str()));
+ match self.novalue {
+ None => (),
+ Some(true) => empty.push_attribute(("novalue", "yes")),
+ Some(false) => empty.push_attribute(("novalue", "no")),
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for RecurrenceModifier {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Expand(exp) => exp.qwrite(xml).await,
+ Self::LimitRecurrenceSet(lrs) => lrs.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for Expand {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("expand");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for LimitRecurrenceSet {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("limit-recurrence-set");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for LimitFreebusySet {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("limit-freebusy-set");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl<E: Extension> QWrite for CalendarSelector<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllProp => {
+ let empty_tag = xml.create_dav_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::PropName => {
+ let empty_tag = xml.create_dav_element("propname");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Prop(prop) => prop.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for CompFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("comp-filter");
+ start.push_attribute(("name", self.name.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start)).await,
+ Some(rules) => {
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for CompFilterRules {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Matches(cfm) => cfm.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for CompFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ if let Some(time_range) = &self.time_range {
+ time_range.qwrite(xml).await?;
+ }
+
+ for prop_item in self.prop_filter.iter() {
+ prop_item.qwrite(xml).await?;
+ }
+ for comp_item in self.comp_filter.iter() {
+ // Required: recursion in an async fn requires boxing
+ // rustc --explain E0733
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a CompFilter,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp_item, xml).await?;
+ }
+ Ok(())
+ }
+}
+
+impl QWrite for PropFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("prop-filter");
+ start.push_attribute(("name", self.name.0.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start.clone())).await,
+ Some(rules) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for PropFilterRules {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Match(prop_match) => prop_match.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for PropFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ if let Some(time_or_text) = &self.time_or_text {
+ time_or_text.qwrite(xml).await?;
+ }
+ for param_item in self.param_filter.iter() {
+ param_item.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+}
+
+impl QWrite for TimeOrText {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Time(time) => time.qwrite(xml).await,
+ Self::Text(txt) => txt.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for TextMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("text-match");
+ if let Some(collation) = &self.collation {
+ start.push_attribute(("collation", collation.as_str()));
+ }
+ match self.negate_condition {
+ None => (),
+ Some(true) => start.push_attribute(("negate-condition", "yes")),
+ Some(false) => start.push_attribute(("negate-condition", "no")),
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.text.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for ParamFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("param-filter");
+ start.push_attribute(("name", self.name.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start)).await,
+ Some(rules) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for ParamFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Match(tm) => tm.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for TimeZone {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("timezone");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.0.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Filter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("filter");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for TimeRange {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("time-range");
+ match self {
+ Self::OnlyStart(start) => empty.push_attribute((
+ "start",
+ format!("{}", start.format(UTC_DATETIME_FMT)).as_str(),
+ )),
+ Self::OnlyEnd(end) => {
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()))
+ }
+ Self::FullRange(start, end) => {
+ empty.push_attribute((
+ "start",
+ format!("{}", start.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()));
+ }
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::types as dav;
+ use chrono::{TimeZone, Utc};
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize(elem: &impl QWrite) -> String {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ return got.into();
+ }
+
+ #[tokio::test]
+ async fn basic_violation() {
+ let got = serialize(&dav::Error::<Calendar>(vec![dav::Violation::Extension(
+ Violation::ResourceMustBeNull,
+ )]))
+ .await;
+
+ let expected = r#"<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <C:resource-must-be-null/>
+</D:error>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query1_req() {
+ let got = serialize(&CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: Some(Comp {
+ name: Component::VCalendar,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(PropKind::Prop(vec![
+ CalProp {
+ name: ComponentProperty("SUMMARY".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("UID".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTSTART".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTEND".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DURATION".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RECURRENCE-ID".into()),
+ novalue: None,
+ },
+ ])),
+ comp_kind: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: None,
+ },
+ ])),
+ }),
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 4, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ prop_filter: vec![],
+ comp_filter: vec![],
+ })),
+ }],
+ })),
+ }),
+ timezone: None,
+ })
+ .await;
+
+ let expected = r#"<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data>
+ <C:comp name="VCALENDAR">
+ <C:prop name="VERSION"/>
+ <C:comp name="VEVENT">
+ <C:prop name="SUMMARY"/>
+ <C:prop name="UID"/>
+ <C:prop name="DTSTART"/>
+ <C:prop name="DTEND"/>
+ <C:prop name="DURATION"/>
+ <C:prop name="RRULE"/>
+ <C:prop name="RDATE"/>
+ <C:prop name="EXRULE"/>
+ <C:prop name="EXDATE"/>
+ <C:prop name="RECURRENCE-ID"/>
+ </C:comp>
+ <C:comp name="VTIMEZONE"/>
+ </C:comp>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060104T000000Z" end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+</C:calendar-query>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query1_res() {
+ let got = serialize(&dav::Multistatus::<Calendar> {
+ extension: None,
+ responses: vec![
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "PLACEHOLDER".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ location: None,
+ error: None,
+ responsedescription: None,
+ },
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "PLACEHOLDER".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ location: None,
+ error: None,
+ responsedescription: None,
+ },
+ ],
+ responsedescription: None,
+ })
+ .await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>&quot;fffff-abcd2&quot;</D:getetag>
+ <C:calendar-data>PLACEHOLDER</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>&quot;fffff-abcd3&quot;</D:getetag>
+ <C:calendar-data>PLACEHOLDER</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+}
diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs
new file mode 100644
index 0000000..a4f6fef
--- /dev/null
+++ b/aero-dav/src/caltypes.rs
@@ -0,0 +1,1500 @@
+#![allow(dead_code)]
+
+use super::types as dav;
+use chrono::{DateTime, Utc};
+
+pub const FLOATING_DATETIME_FMT: &str = "%Y%m%dT%H%M%S";
+pub const UTC_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ";
+
+//@FIXME ACL (rfc3744) is missing, required
+//@FIXME Versioning (rfc3253) is missing, required
+//@FIXME WebDAV sync (rfc6578) is missing, optional
+// For reference, SabreDAV guide gives high-level & real-world overview:
+// https://sabre.io/dav/building-a-caldav-client/
+// For reference, non-official extensions documented by SabreDAV:
+// https://github.com/apple/ccs-calendarserver/tree/master/doc/Extensions
+
+// ----- Root elements -----
+
+// --- (MKCALENDAR PART) ---
+
+/// If a request body is included, it MUST be a CALDAV:mkcalendar XML
+/// element. Instruction processing MUST occur in the order
+/// instructions are received (i.e., from top to bottom).
+/// Instructions MUST either all be executed or none executed. Thus,
+/// if any error occurs during processing, all executed instructions
+/// MUST be undone and a proper error result returned. Instruction
+/// processing details can be found in the definition of the DAV:set
+/// instruction in Section 12.13.2 of [RFC2518].
+///
+/// ```xmlschema
+/// <!ELEMENT mkcalendar (DAV:set)>
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>);
+
+/// If a response body for a successful request is included, it MUST
+/// be a CALDAV:mkcalendar-response XML element.
+///
+/// <!ELEMENT mkcalendar-response ANY>
+///
+/// ----
+///
+/// ANY is not satisfying, so looking at RFC5689
+/// https://www.rfc-editor.org/rfc/rfc5689.html#section-5.2
+///
+/// Definition:
+///
+/// <!ELEMENT mkcol-response (propstat+)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct MkCalendarResponse<E: dav::Extension>(pub Vec<dav::PropStat<E>>);
+
+// --- (REPORT PART) ---
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Query,
+ Multiget,
+ FreeBusy,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Query(CalendarQuery<E>),
+ Multiget(CalendarMultiget<E>),
+ FreeBusy(FreeBusyQuery),
+}
+
+/// Name: calendar-query
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines a report for querying calendar object resources.
+///
+/// Description: See Section 7.8.
+///
+/// Definition:
+///
+/// <!ELEMENT calendar-query ((DAV:allprop |
+/// DAV:propname |
+/// DAV:prop)?, filter, timezone?)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarQuery<E: dav::Extension> {
+ pub selector: Option<CalendarSelector<E>>,
+ pub filter: Filter,
+ pub timezone: Option<TimeZone>,
+}
+
+/// Name: calendar-multiget
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: CalDAV report used to retrieve specific calendar object
+/// resources.
+///
+/// Description: See Section 7.9.
+///
+/// Definition:
+///
+/// <!ELEMENT calendar-multiget ((DAV:allprop |
+/// DAV:propname |
+/// DAV:prop)?, DAV:href+)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarMultiget<E: dav::Extension> {
+ pub selector: Option<CalendarSelector<E>>,
+ pub href: Vec<dav::Href>,
+}
+
+/// Name: free-busy-query
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: CalDAV report used to generate a VFREEBUSY to determine
+/// busy time over a specific time range.
+///
+/// Description: See Section 7.10.
+///
+/// Definition:
+/// <!ELEMENT free-busy-query (time-range)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct FreeBusyQuery(pub TimeRange);
+
+// ----- Hooks -----
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Calendar,
+}
+
+/// Check the matching Property object for documentation
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ CalendarHomeSet,
+ CalendarDescription,
+ CalendarTimezone,
+ SupportedCalendarComponentSet,
+ SupportedCalendarData,
+ MaxResourceSize,
+ MinDateTime,
+ MaxDateTime,
+ MaxInstances,
+ MaxAttendeesPerInstance,
+ SupportedCollationSet,
+ CalendarData(CalendarDataRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ /// Name: calendar-home-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Identifies the URL of any WebDAV collections that contain
+ /// calendar collections owned by the associated principal resource.
+ ///
+ /// Conformance: This property SHOULD be defined on a principal
+ /// resource. If defined, it MAY be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:calendar-home-set property is meant to allow
+ /// users to easily find the calendar collections owned by the
+ /// principal. Typically, users will group all the calendar
+ /// collections that they own under a common collection. This
+ /// property specifies the URL of collections that are either calendar
+ /// collections or ordinary collections that have child or descendant
+ /// calendar collections owned by the principal.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT calendar-home-set (DAV:href*)>
+ CalendarHomeSet(dav::Href),
+
+ /// Name: calendar-description
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a human-readable description of the calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MAY be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]). An xml:lang attribute indicating the human
+ /// language of the description SHOULD be set for this property by
+ /// clients or through server provisioning. Servers MUST return any
+ /// xml:lang attribute if set for the property.
+ ///
+ /// Description: If present, the property contains a description of the
+ /// calendar collection that is suitable for presentation to a user.
+ /// If not present, the client should assume no description for the
+ /// calendar collection.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT calendar-description (#PCDATA)>
+ /// PCDATA value: string
+ ///
+ /// Example:
+ ///
+ /// <C:calendar-description xml:lang="fr-CA"
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav"
+ /// >Calendrier de Mathilde Desruisseaux</C:calendar-description>
+ CalendarDescription { lang: Option<String>, text: String },
+
+ /// 5.2.2. CALDAV:calendar-timezone Property
+ ///
+ /// Name: calendar-timezone
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies a time zone on a calendar collection.
+ ///
+ /// Conformance: This property SHOULD be defined on all calendar
+ /// collections. If defined, it SHOULD NOT be returned by a PROPFIND
+ /// DAV:allprop request (as defined in Section 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:calendar-timezone property is used to
+ /// specify the time zone the server should rely on to resolve "date"
+ /// values and "date with local time" values (i.e., floating time) to
+ /// "date with UTC time" values. The server will require this
+ /// information to determine if a calendar component scheduled with
+ /// "date" values or "date with local time" values overlaps a CALDAV:
+ /// time-range specified in a CALDAV:calendar-query REPORT. The
+ /// server will also require this information to compute the proper
+ /// FREEBUSY time period as "date with UTC time" in the VFREEBUSY
+ /// component returned in a response to a CALDAV:free-busy-query
+ /// REPORT request that takes into account calendar components
+ /// scheduled with "date" values or "date with local time" values. In
+ /// the absence of this property, the server MAY rely on the time zone
+ /// of their choice.
+ ///
+ /// Note: The iCalendar data embedded within the CALDAV:calendar-
+ /// timezone XML element MUST follow the standard XML character data
+ /// encoding rules, including use of &lt;, &gt;, &amp; etc. entity
+ /// encoding or the use of a <![CDATA[ ... ]]> construct. In the
+ /// later case, the iCalendar data cannot contain the character
+ /// sequence "]]>", which is the end delimiter for the CDATA section.
+ ///
+ /// Definition:
+ ///
+ /// ```xmlschema
+ /// <!ELEMENT calendar-timezone (#PCDATA)>
+ /// PCDATA value: an iCalendar object with exactly one VTIMEZONE component.
+ /// ```
+ ///
+ /// Example:
+ ///
+ /// ```xmlschema
+ /// <C:calendar-timezone
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
+ /// PRODID:-//Example Corp.//CalDAV Client//EN
+ /// VERSION:2.0
+ /// BEGIN:VTIMEZONE
+ /// TZID:US-Eastern
+ /// LAST-MODIFIED:19870101T000000Z
+ /// BEGIN:STANDARD
+ /// DTSTART:19671029T020000
+ /// RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ /// TZOFFSETFROM:-0400
+ /// TZOFFSETTO:-0500
+ /// TZNAME:Eastern Standard Time (US &amp; Canada)
+ /// END:STANDARD
+ /// BEGIN:DAYLIGHT
+ /// DTSTART:19870405T020000
+ /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ /// TZOFFSETFROM:-0500
+ /// TZOFFSETTO:-0400
+ /// TZNAME:Eastern Daylight Time (US &amp; Canada)
+ /// END:DAYLIGHT
+ /// END:VTIMEZONE
+ /// END:VCALENDAR
+ /// </C:calendar-timezone>
+ /// ```
+ //@FIXME we might want to put a buffer here or an iCal parsed object
+ CalendarTimezone(String),
+
+ /// Name: supported-calendar-component-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies the calendar component types (e.g., VEVENT,
+ /// VTODO, etc.) that calendar object resources can contain in the
+ /// calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-calendar-component-set property is
+ /// used to specify restrictions on the calendar component types that
+ /// calendar object resources may contain in a calendar collection.
+ /// Any attempt by the client to store calendar object resources with
+ /// component types not listed in this property, if it exists, MUST
+ /// result in an error, with the CALDAV:supported-calendar-component
+ /// precondition (Section 5.3.2.1) being violated. Since this
+ /// property is protected, it cannot be changed by clients using a
+ /// PROPPATCH request. However, clients can initialize the value of
+ /// this property when creating a new calendar collection with
+ /// MKCALENDAR. The empty-element tag <C:comp name="VTIMEZONE"/> MUST
+ /// only be specified if support for calendar object resources that
+ /// only contain VTIMEZONE components is provided or desired. Support
+ /// for VTIMEZONE components in calendar object resources that contain
+ /// VEVENT or VTODO components is always assumed. In the absence of
+ /// this property, the server MUST accept all component types, and the
+ /// client can assume that all component types are accepted.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-calendar-component-set (comp+)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-calendar-component-set
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:comp name="VEVENT"/>
+ /// <C:comp name="VTODO"/>
+ /// </C:supported-calendar-component-set>
+ SupportedCalendarComponentSet(Vec<CompSupport>),
+
+ /// Name: supported-calendar-data
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies what media types are allowed for calendar object
+ /// resources in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-calendar-data property is used to
+ /// specify the media type supported for the calendar object resources
+ /// contained in a given calendar collection (e.g., iCalendar version
+ /// 2.0). Any attempt by the client to store calendar object
+ /// resources with a media type not listed in this property MUST
+ /// result in an error, with the CALDAV:supported-calendar-data
+ /// precondition (Section 5.3.2.1) being violated. In the absence of
+ /// this property, the server MUST only accept data with the media
+ /// type "text/calendar" and iCalendar version 2.0, and clients can
+ /// assume that the server will only accept this data.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-calendar-data (calendar-data+)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-calendar-data
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:calendar-data content-type="text/calendar" version="2.0"/>
+ /// </C:supported-calendar-data>
+ ///
+ /// -----
+ ///
+ /// <!ELEMENT calendar-data EMPTY>
+ ///
+ /// when nested in the CALDAV:supported-calendar-data property
+ /// to specify a supported media type for calendar object
+ /// resources;
+ SupportedCalendarData(Vec<CalendarDataEmpty>),
+
+ /// Name: max-resource-size
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum size of a
+ /// resource in octets that the server is willing to accept when a
+ /// calendar object resource is stored in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-resource-size is used to specify a
+ /// numeric value that represents the maximum size in octets that the
+ /// server is willing to accept when a calendar object resource is
+ /// stored in a calendar collection. Any attempt to store a calendar
+ /// object resource exceeding this size MUST result in an error, with
+ /// the CALDAV:max-resource-size precondition (Section 5.3.2.1) being
+ /// violated. In the absence of this property, the client can assume
+ /// that the server will allow storing a resource of any reasonable
+ /// size.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-resource-size (#PCDATA)>
+ /// PCDATA value: a numeric value (positive integer)
+ ///
+ /// Example:
+ ///
+ /// <C:max-resource-size xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 102400
+ /// </C:max-resource-size>
+ MaxResourceSize(u64),
+
+ /// CALDAV:min-date-time Property
+ ///
+ /// Name: min-date-time
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a DATE-TIME value indicating the earliest date and
+ /// time (in UTC) that the server is willing to accept for any DATE or
+ /// DATE-TIME value in a calendar object resource stored in a calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:min-date-time is used to specify an
+ /// iCalendar DATE-TIME value in UTC that indicates the earliest
+ /// inclusive date that the server is willing to accept for any
+ /// explicit DATE or DATE-TIME value in a calendar object resource
+ /// stored in a calendar collection. Any attempt to store a calendar
+ /// object resource using a DATE or DATE-TIME value earlier than this
+ /// value MUST result in an error, with the CALDAV:min-date-time
+ /// precondition (Section 5.3.2.1) being violated. Note that servers
+ /// MUST accept recurring components that specify instances beyond
+ /// this limit, provided none of those instances have been overridden.
+ /// In that case, the server MAY simply ignore those instances outside
+ /// of the acceptable range when processing reports on the calendar
+ /// object resource. In the absence of this property, the client can
+ /// assume any valid iCalendar date may be used at least up to the
+ /// CALDAV:max-date-time value, if that is defined.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT min-date-time (#PCDATA)>
+ /// PCDATA value: an iCalendar format DATE-TIME value in UTC
+ ///
+ /// Example:
+ ///
+ /// <C:min-date-time xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 19000101T000000Z
+ /// </C:min-date-time>
+ MinDateTime(DateTime<Utc>),
+
+ /// CALDAV:max-date-time Property
+ ///
+ /// Name: max-date-time
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a DATE-TIME value indicating the latest date and
+ /// time (in UTC) that the server is willing to accept for any DATE or
+ /// DATE-TIME value in a calendar object resource stored in a calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-date-time is used to specify an
+ /// iCalendar DATE-TIME value in UTC that indicates the inclusive
+ /// latest date that the server is willing to accept for any date or
+ /// time value in a calendar object resource stored in a calendar
+ /// collection. Any attempt to store a calendar object resource using
+ /// a DATE or DATE-TIME value later than this value MUST result in an
+ /// error, with the CALDAV:max-date-time precondition
+ /// (Section 5.3.2.1) being violated. Note that servers MUST accept
+ /// recurring components that specify instances beyond this limit,
+ /// provided none of those instances have been overridden. In that
+ /// case, the server MAY simply ignore those instances outside of the
+ /// acceptable range when processing reports on the calendar object
+ /// resource. In the absence of this property, the client can assume
+ /// any valid iCalendar date may be used at least down to the CALDAV:
+ /// min-date-time value, if that is defined.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-date-time (#PCDATA)>
+ /// PCDATA value: an iCalendar format DATE-TIME value in UTC
+ ///
+ /// Example:
+ ///
+ /// <C:max-date-time xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 20491231T235959Z
+ /// </C:max-date-time>
+ MaxDateTime(DateTime<Utc>),
+
+ /// CALDAV:max-instances Property
+ ///
+ /// Name: max-instances
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum number of
+ /// recurrence instances that a calendar object resource stored in a
+ /// calendar collection can generate.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-instances is used to specify a numeric
+ /// value that indicates the maximum number of recurrence instances
+ /// that a calendar object resource stored in a calendar collection
+ /// can generate. Any attempt to store a calendar object resource
+ /// with a recurrence pattern that generates more instances than this
+ /// value MUST result in an error, with the CALDAV:max-instances
+ /// precondition (Section 5.3.2.1) being violated. In the absence of
+ /// this property, the client can assume that the server has no limits
+ /// on the number of recurrence instances it can handle or expand.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-instances (#PCDATA)>
+ /// PCDATA value: a numeric value (integer greater than zero)
+ ///
+ /// Example:
+ ///
+ /// <C:max-instances xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 100
+ /// </C:max-instances>
+ MaxInstances(u64),
+
+ /// CALDAV:max-attendees-per-instance Property
+ ///
+ /// Name: max-attendees-per-instance
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum number of
+ /// ATTENDEE properties in any instance of a calendar object resource
+ /// stored in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-attendees-per-instance is used to
+ /// specify a numeric value that indicates the maximum number of
+ /// iCalendar ATTENDEE properties on any one instance of a calendar
+ /// object resource stored in a calendar collection. Any attempt to
+ /// store a calendar object resource with more ATTENDEE properties per
+ /// instance than this value MUST result in an error, with the CALDAV:
+ /// max-attendees-per-instance precondition (Section 5.3.2.1) being
+ /// violated. In the absence of this property, the client can assume
+ /// that the server can handle any number of ATTENDEE properties in a
+ /// calendar component.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-attendees-per-instance (#PCDATA)>
+ /// PCDATA value: a numeric value (integer greater than zero)
+ ///
+ /// Example:
+ ///
+ /// <C:max-attendees-per-instance
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 25
+ /// </C:max-attendees-per-instance>
+ MaxAttendeesPerInstance(u64),
+
+ /// Name: supported-collation-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Identifies the set of collations supported by the server
+ /// for text matching operations.
+ ///
+ /// Conformance: This property MUST be defined on any resource that
+ /// supports a report that does text matching. If defined, it MUST be
+ /// protected and SHOULD NOT be returned by a PROPFIND DAV:allprop
+ /// request (as defined in Section 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-collation-set property contains
+ /// zero or more CALDAV:supported-collation elements, which specify
+ /// the collection identifiers of the collations supported by the
+ /// server.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-collation-set (supported-collation*)>
+ /// <!ELEMENT supported-collation (#PCDATA)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-collation-set
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:supported-collation>i;ascii-casemap</C:supported-collation>
+ /// <C:supported-collation>i;octet</C:supported-collation>
+ /// </C:supported-collation-set>
+ SupportedCollationSet(Vec<SupportedCollation>),
+
+ /// Name: calendar-data
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specified one of the following:
+ ///
+ /// 1. A supported media type for calendar object resources when
+ /// nested in the CALDAV:supported-calendar-data property;
+ ///
+ /// 2. The parts of a calendar object resource should be returned by
+ /// a calendaring report;
+ ///
+ /// 3. The content of a calendar object resource in a response to a
+ /// calendaring report.
+ ///
+ /// Description: When nested in the CALDAV:supported-calendar-data
+ /// property, the CALDAV:calendar-data XML element specifies a media
+ /// type supported by the CalDAV server for calendar object resources.
+ ///
+ /// When used in a calendaring REPORT request, the CALDAV:calendar-
+ /// data XML element specifies which parts of calendar object
+ /// resources need to be returned in the response. If the CALDAV:
+ /// calendar-data XML element doesn't contain any CALDAV:comp element,
+ /// calendar object resources will be returned in their entirety.
+ ///
+ /// Finally, when used in a calendaring REPORT response, the CALDAV:
+ /// calendar-data XML element specifies the content of a calendar
+ /// object resource. Given that XML parsers normalize the two-
+ /// character sequence CRLF (US-ASCII decimal 13 and US-ASCII decimal
+ /// 10) to a single LF character (US-ASCII decimal 10), the CR
+ /// character (US-ASCII decimal 13) MAY be omitted in calendar object
+ /// resources specified in the CALDAV:calendar-data XML element.
+ /// Furthermore, calendar object resources specified in the CALDAV:
+ /// calendar-data XML element MAY be invalid per their media type
+ /// specification if the CALDAV:calendar-data XML element part of the
+ /// calendaring REPORT request did not specify required properties
+ /// (e.g., UID, DTSTAMP, etc.), or specified a CALDAV:prop XML element
+ /// with the "novalue" attribute set to "yes".
+ ///
+ /// Note: The CALDAV:calendar-data XML element is specified in requests
+ /// and responses inside the DAV:prop XML element as if it were a
+ /// WebDAV property. However, the CALDAV:calendar-data XML element is
+ /// not a WebDAV property and, as such, is not returned in PROPFIND
+ /// responses, nor used in PROPPATCH requests.
+ ///
+ /// Note: The iCalendar data embedded within the CALDAV:calendar-data
+ /// XML element MUST follow the standard XML character data encoding
+ /// rules, including use of &lt;, &gt;, &amp; etc. entity encoding or
+ /// the use of a <![CDATA[ ... ]]> construct. In the later case, the
+ /// iCalendar data cannot contain the character sequence "]]>", which
+ /// is the end delimiter for the CDATA section.
+ CalendarData(CalendarDataPayload),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Violation {
+ /// (DAV:resource-must-be-null): A resource MUST NOT exist at the
+ /// Request-URI;
+ ResourceMustBeNull,
+
+ /// (CALDAV:calendar-collection-location-ok): The Request-URI MUST
+ /// identify a location where a calendar collection can be created;
+ CalendarCollectionLocationOk,
+
+ /// (CALDAV:valid-calendar-data): The time zone specified in CALDAV:
+ /// calendar-timezone property MUST be a valid iCalendar object
+ /// containing a single valid VTIMEZONE component.
+ ValidCalendarData,
+
+ ///@FIXME should not be here but in RFC3744
+ /// !!! ERRATA 1002 !!!
+ /// (DAV:need-privileges): The DAV:bind privilege MUST be granted to
+ /// the current user on the parent collection of the Request-URI.
+ NeedPrivileges,
+
+ /// (CALDAV:initialize-calendar-collection): A new calendar collection
+ /// exists at the Request-URI. The DAV:resourcetype of the calendar
+ /// collection MUST contain both DAV:collection and CALDAV:calendar
+ /// XML elements.
+ InitializeCalendarCollection,
+
+ /// (CALDAV:supported-calendar-data): The resource submitted in the
+ /// PUT request, or targeted by a COPY or MOVE request, MUST be a
+ /// supported media type (i.e., iCalendar) for calendar object
+ /// resources;
+ SupportedCalendarData,
+
+ /// (CALDAV:valid-calendar-object-resource): The resource submitted in
+ /// the PUT request, or targeted by a COPY or MOVE request, MUST obey
+ /// all restrictions specified in Section 4.1 (e.g., calendar object
+ /// resources MUST NOT contain more than one type of calendar
+ /// component, calendar object resources MUST NOT specify the
+ /// iCalendar METHOD property, etc.);
+ ValidCalendarObjectResource,
+
+ /// (CALDAV:supported-calendar-component): The resource submitted in
+ /// the PUT request, or targeted by a COPY or MOVE request, MUST
+ /// contain a type of calendar component that is supported in the
+ /// targeted calendar collection;
+ SupportedCalendarComponent,
+
+ /// (CALDAV:no-uid-conflict): The resource submitted in the PUT
+ /// request, or targeted by a COPY or MOVE request, MUST NOT specify
+ /// an iCalendar UID property value already in use in the targeted
+ /// calendar collection or overwrite an existing calendar object
+ /// resource with one that has a different UID property value.
+ /// Servers SHOULD report the URL of the resource that is already
+ /// making use of the same UID property value in the DAV:href element;
+ ///
+ /// <!ELEMENT no-uid-conflict (DAV:href)>
+ NoUidConflict(dav::Href),
+
+ /// (CALDAV:max-resource-size): The resource submitted in the PUT
+ /// request, or targeted by a COPY or MOVE request, MUST have an octet
+ /// size less than or equal to the value of the CALDAV:max-resource-
+ /// size property value (Section 5.2.5) on the calendar collection
+ /// where the resource will be stored;
+ MaxResourceSize,
+
+ /// (CALDAV:min-date-time): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST have all of its
+ /// iCalendar DATE or DATE-TIME property values (for each recurring
+ /// instance) greater than or equal to the value of the CALDAV:min-
+ /// date-time property value (Section 5.2.6) on the calendar
+ /// collection where the resource will be stored;
+ MinDateTime,
+
+ /// (CALDAV:max-date-time): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST have all of its
+ /// iCalendar DATE or DATE-TIME property values (for each recurring
+ /// instance) less than the value of the CALDAV:max-date-time property
+ /// value (Section 5.2.7) on the calendar collection where the
+ /// resource will be stored;
+ MaxDateTime,
+
+ /// (CALDAV:max-instances): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST generate a number of
+ /// recurring instances less than or equal to the value of the CALDAV:
+ /// max-instances property value (Section 5.2.8) on the calendar
+ /// collection where the resource will be stored;
+ MaxInstances,
+
+ /// (CALDAV:max-attendees-per-instance): The resource submitted in the
+ /// PUT request, or targeted by a COPY or MOVE request, MUST have a
+ /// number of ATTENDEE properties on any one instance less than or
+ /// equal to the value of the CALDAV:max-attendees-per-instance
+ /// property value (Section 5.2.9) on the calendar collection where
+ /// the resource will be stored;
+ MaxAttendeesPerInstance,
+
+ /// (CALDAV:valid-filter): The CALDAV:filter XML element (see
+ /// Section 9.7) specified in the REPORT request MUST be valid. For
+ /// instance, a CALDAV:filter cannot nest a <C:comp name="VEVENT">
+ /// element in a <C:comp name="VTODO"> element, and a CALDAV:filter
+ /// cannot nest a <C:time-range start="..." end="..."> element in a
+ /// <C:prop name="SUMMARY"> element.
+ ValidFilter,
+
+ /// (CALDAV:supported-filter): The CALDAV:comp-filter (see
+ /// Section 9.7.1), CALDAV:prop-filter (see Section 9.7.2), and
+ /// CALDAV:param-filter (see Section 9.7.3) XML elements used in the
+ /// CALDAV:filter XML element (see Section 9.7) in the REPORT request
+ /// only make reference to components, properties, and parameters for
+ /// which queries are supported by the server, i.e., if the CALDAV:
+ /// filter element attempts to reference an unsupported component,
+ /// property, or parameter, this precondition is violated. Servers
+ /// SHOULD report the CALDAV:comp-filter, CALDAV:prop-filter, or
+ /// CALDAV:param-filter for which it does not provide support.
+ ///
+ /// <!ELEMENT supported-filter (comp-filter*,
+ /// prop-filter*,
+ /// param-filter*)>
+ SupportedFilter {
+ comp: Vec<CompFilter>,
+ prop: Vec<PropFilter>,
+ param: Vec<ParamFilter>,
+ },
+
+ /// (DAV:number-of-matches-within-limits): The number of matching
+ /// calendar object resources must fall within server-specific,
+ /// predefined limits. For example, this condition might be triggered
+ /// if a search specification would cause the return of an extremely
+ /// large number of responses.
+ NumberOfMatchesWithinLimits,
+}
+
+// -------- Inner XML elements ---------
+
+/// Some of the reports defined in this section do text matches of
+/// character strings provided by the client and are compared to stored
+/// calendar data. Since iCalendar data is, by default, encoded in the
+/// UTF-8 charset and may include characters outside the US-ASCII charset
+/// range in some property and parameter values, there is a need to
+/// ensure that text matching follows well-defined rules.
+///
+/// To deal with this, this specification makes use of the IANA Collation
+/// Registry defined in [RFC4790] to specify collations that may be used
+/// to carry out the text comparison operations with a well-defined rule.
+///
+/// The comparisons used in CalDAV are all "substring" matches, as per
+/// [RFC4790], Section 4.2. Collations supported by the server MUST
+/// support "substring" match operations.
+///
+/// CalDAV servers are REQUIRED to support the "i;ascii-casemap" and
+/// "i;octet" collations, as described in [RFC4790], and MAY support
+/// other collations.
+///
+/// Servers MUST advertise the set of collations that they support via
+/// the CALDAV:supported-collation-set property defined on any resource
+/// that supports reports that use collations.
+///
+/// Clients MUST only use collations from the list advertised by the
+/// server.
+///
+/// In the absence of a collation explicitly specified by the client, or
+/// if the client specifies the "default" collation identifier (as
+/// defined in [RFC4790], Section 3.1), the server MUST default to using
+/// "i;ascii-casemap" as the collation.
+///
+/// Wildcards (as defined in [RFC4790], Section 3.2) MUST NOT be used in
+/// the collation identifier.
+///
+/// If the client chooses a collation not supported by the server, the
+/// server MUST respond with a CALDAV:supported-collation precondition
+/// error response.
+#[derive(Debug, PartialEq, Clone)]
+pub struct SupportedCollation(pub Collation);
+
+/// <!ELEMENT calendar-data (#PCDATA)>
+/// PCDATA value: iCalendar object
+///
+/// when nested in the DAV:prop XML element in a calendaring
+/// REPORT response to specify the content of a returned
+/// calendar object resource.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataPayload {
+ pub mime: Option<CalendarDataSupport>,
+ pub payload: String,
+}
+
+/// <!ELEMENT calendar-data (comp?,
+/// (expand | limit-recurrence-set)?,
+/// limit-freebusy-set?)>
+///
+/// when nested in the DAV:prop XML element in a calendaring
+/// REPORT request to specify which parts of calendar object
+/// resources should be returned in the response;
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct CalendarDataRequest {
+ pub mime: Option<CalendarDataSupport>,
+ pub comp: Option<Comp>,
+ pub recurrence: Option<RecurrenceModifier>,
+ pub limit_freebusy_set: Option<LimitFreebusySet>,
+}
+
+/// calendar-data specialization for Property
+///
+/// <!ELEMENT calendar-data EMPTY>
+///
+/// when nested in the CALDAV:supported-calendar-data property
+/// to specify a supported media type for calendar object
+/// resources;
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataEmpty(pub Option<CalendarDataSupport>);
+
+/// <!ATTLIST calendar-data content-type CDATA "text/calendar"
+/// version CDATA "2.0">
+/// content-type value: a MIME media type
+/// version value: a version string
+/// attributes can be used on all three variants of the
+/// CALDAV:calendar-data XML element.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataSupport {
+ pub content_type: String,
+ pub version: String,
+}
+
+/// Name: comp
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines which component types to return.
+///
+/// Description: The name value is a calendar component name (e.g.,
+/// VEVENT).
+///
+/// Definition:
+///
+/// <!ELEMENT comp ((allprop | prop*), (allcomp | comp*))>
+/// <!ATTLIST comp name CDATA #REQUIRED>
+/// name value: a calendar component name
+///
+/// Note: The CALDAV:prop and CALDAV:allprop elements have the same name
+/// as the DAV:prop and DAV:allprop elements defined in [RFC2518].
+/// However, the CALDAV:prop and CALDAV:allprop elements are defined
+/// in the "urn:ietf:params:xml:ns:caldav" namespace instead of the
+/// "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub struct Comp {
+ pub name: Component,
+ pub prop_kind: Option<PropKind>,
+ pub comp_kind: Option<CompKind>,
+}
+
+/// For SupportedCalendarComponentSet
+///
+/// Definition:
+///
+/// <!ELEMENT supported-calendar-component-set (comp+)>
+///
+/// Example:
+///
+/// <C:supported-calendar-component-set
+/// xmlns:C="urn:ietf:params:xml:ns:caldav">
+/// <C:comp name="VEVENT"/>
+/// <C:comp name="VTODO"/>
+/// </C:supported-calendar-component-set>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompSupport(pub Component);
+
+/// Name: allcomp
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that all components shall be returned.
+///
+/// Description: The CALDAV:allcomp XML element can be used when the
+/// client wants all types of components returned by a calendaring
+/// REPORT request.
+///
+/// Definition:
+///
+/// <!ELEMENT allcomp EMPTY>
+#[derive(Debug, PartialEq, Clone)]
+pub enum CompKind {
+ AllComp,
+ Comp(Vec<Comp>),
+}
+
+/// Name: allprop
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that all properties shall be returned.
+///
+/// Description: The CALDAV:allprop XML element can be used when the
+/// client wants all properties of components returned by a
+/// calendaring REPORT request.
+///
+/// Definition:
+///
+/// <!ELEMENT allprop EMPTY>
+///
+/// Note: The CALDAV:allprop element has the same name as the DAV:
+/// allprop element defined in [RFC2518]. However, the CALDAV:allprop
+/// element is defined in the "urn:ietf:params:xml:ns:caldav"
+/// namespace instead of the "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropKind {
+ AllProp,
+ Prop(Vec<CalProp>),
+}
+
+/// Name: prop
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines which properties to return in the response.
+///
+/// Description: The "name" attribute specifies the name of the calendar
+/// property to return (e.g., ATTENDEE). The "novalue" attribute can
+/// be used by clients to request that the actual value of the
+/// property not be returned (if the "novalue" attribute is set to
+/// "yes"). In that case, the server will return just the iCalendar
+/// property name and any iCalendar parameters and a trailing ":"
+/// without the subsequent value data.
+///
+/// Definition:
+/// <!ELEMENT prop EMPTY>
+/// <!ATTLIST prop name CDATA #REQUIRED novalue (yes | no) "no">
+/// name value: a calendar property name
+/// novalue value: "yes" or "no"
+///
+/// Note: The CALDAV:prop element has the same name as the DAV:prop
+/// element defined in [RFC2518]. However, the CALDAV:prop element is
+/// defined in the "urn:ietf:params:xml:ns:caldav" namespace instead
+/// of the "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalProp {
+ pub name: ComponentProperty,
+ pub novalue: Option<bool>,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum RecurrenceModifier {
+ Expand(Expand),
+ LimitRecurrenceSet(LimitRecurrenceSet),
+}
+
+/// Name: expand
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Forces the server to expand recurring components into
+/// individual recurrence instances.
+///
+/// Description: The CALDAV:expand XML element specifies that for a
+/// given calendaring REPORT request, the server MUST expand the
+/// recurrence set into calendar components that define exactly one
+/// recurrence instance, and MUST return only those whose scheduled
+/// time intersect a specified time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as date with UTC
+/// time value. The value of the "end" attribute MUST be greater than
+/// the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if a recurrence instance intersects the
+/// specified time range.
+///
+/// Recurring components, other than the initial instance, MUST
+/// include a RECURRENCE-ID property indicating which instance they
+/// refer to.
+///
+/// The returned calendar components MUST NOT use recurrence
+/// properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT
+/// have reference to or include VTIMEZONE components. Date and local
+/// time with reference to time zone information MUST be converted
+/// into date with UTC time.
+///
+/// Definition:
+///
+/// <!ELEMENT expand EMPTY>
+/// <!ATTLIST expand start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct Expand(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// CALDAV:limit-recurrence-set XML Element
+///
+/// Name: limit-recurrence-set
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a time range to limit the set of "overridden
+/// components" returned by the server.
+///
+/// Description: The CALDAV:limit-recurrence-set XML element specifies
+/// that for a given calendaring REPORT request, the server MUST
+/// return, in addition to the "master component", only the
+/// "overridden components" that impact a specified time range. An
+/// overridden component impacts a time range if its current start and
+/// end times overlap the time range, or if the original start and end
+/// times -- the ones that would have been used if the instance were
+/// not overridden -- overlap the time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as date with UTC
+/// time value. The value of the "end" attribute MUST be greater than
+/// the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if the current or original scheduled time of an
+/// "overridden" recurrence instance intersects the specified time
+/// range.
+///
+/// Overridden components that have a RANGE parameter on their
+/// RECURRENCE-ID property may specify one or more instances in the
+/// recurrence set, and some of those instances may fall within the
+/// specified time range or may have originally fallen within the
+/// specified time range prior to being overridden. If that is the
+/// case, the overridden component MUST be included in the results, as
+/// it has a direct impact on the interpretation of instances within
+/// the specified time range.
+///
+/// Definition:
+///
+/// <!ELEMENT limit-recurrence-set EMPTY>
+/// <!ATTLIST limit-recurrence-set start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct LimitRecurrenceSet(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// Name: limit-freebusy-set
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a time range to limit the set of FREEBUSY values
+/// returned by the server.
+///
+/// Description: The CALDAV:limit-freebusy-set XML element specifies
+/// that for a given calendaring REPORT request, the server MUST only
+/// return the FREEBUSY property values of a VFREEBUSY component that
+/// intersects a specified time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as "date with UTC
+/// time" value. The value of the "end" attribute MUST be greater
+/// than the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if a FREEBUSY property value intersects the
+/// specified time range.
+///
+/// Definition:
+/// <!ELEMENT limit-freebusy-set EMPTY>
+/// <!ATTLIST limit-freebusy-set start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct LimitFreebusySet(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// Used by CalendarQuery & CalendarMultiget
+#[derive(Debug, PartialEq, Clone)]
+pub enum CalendarSelector<E: dav::Extension> {
+ AllProp,
+ PropName,
+ Prop(dav::PropName<E>),
+}
+
+/// Name: comp-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies search criteria on calendar components.
+///
+/// Description: The CALDAV:comp-filter XML element specifies a query
+/// targeted at the calendar object (i.e., VCALENDAR) or at a specific
+/// calendar component type (e.g., VEVENT). The scope of the
+/// CALDAV:comp-filter XML element is the calendar object when used as
+/// a child of the CALDAV:filter XML element. The scope of the
+/// CALDAV:comp-filter XML element is the enclosing calendar component
+/// when used as a child of another CALDAV:comp-filter XML element. A
+/// CALDAV:comp-filter is said to match if:
+///
+/// * The CALDAV:comp-filter XML element is empty and the calendar
+/// object or calendar component type specified by the "name"
+/// attribute exists in the current scope;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element contains a CALDAV:is-not-
+/// defined XML element and the calendar object or calendar
+/// component type specified by the "name" attribute does not exist
+/// in the current scope;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element contains a CALDAV:time-range
+/// XML element and at least one recurrence instance in the
+/// targeted calendar component is scheduled to overlap the
+/// specified time range, and all specified CALDAV:prop-filter and
+/// CALDAV:comp-filter child XML elements also match the targeted
+/// calendar component;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element only contains CALDAV:prop-
+/// filter and CALDAV:comp-filter child XML elements that all match
+/// the targeted calendar component.
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT comp-filter (is-not-defined | (time-range?,
+/// prop-filter*, comp-filter*))>
+///
+/// <!ATTLIST comp-filter name CDATA #REQUIRED>
+/// name value: a calendar object or calendar component
+/// type (e.g., VEVENT)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompFilter {
+ pub name: Component,
+ // Option 1 = None, Option 2, 3, 4 = Some
+ pub additional_rules: Option<CompFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum CompFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Matches(CompFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompFilterMatch {
+ pub time_range: Option<TimeRange>,
+ pub prop_filter: Vec<PropFilter>,
+ pub comp_filter: Vec<CompFilter>,
+}
+
+/// Name: prop-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies search criteria on calendar properties.
+///
+/// Description: The CALDAV:prop-filter XML element specifies a query
+/// targeted at a specific calendar property (e.g., CATEGORIES) in the
+/// scope of the enclosing calendar component. A calendar property is
+/// said to match a CALDAV:prop-filter if:
+///
+/// * The CALDAV:prop-filter XML element is empty and a property of
+/// the type specified by the "name" attribute exists in the
+/// enclosing calendar component;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:is-not-
+/// defined XML element and no property of the type specified by
+/// the "name" attribute exists in the enclosing calendar
+/// component;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:time-range
+/// XML element and the property value overlaps the specified time
+/// range, and all specified CALDAV:param-filter child XML elements
+/// also match the targeted property;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:text-match
+/// XML element and the property value matches it, and all
+/// specified CALDAV:param-filter child XML elements also match the
+/// targeted property;
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT prop-filter (is-not-defined |
+/// ((time-range | text-match)?,
+/// param-filter*))>
+///
+/// <!ATTLIST prop-filter name CDATA #REQUIRED>
+/// name value: a calendar property name (e.g., ATTENDEE)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropFilter {
+ pub name: ComponentProperty,
+ // None = Option 1, Some() = Option 2, 3 & 4
+ pub additional_rules: Option<PropFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Match(PropFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropFilterMatch {
+ pub time_or_text: Option<TimeOrText>,
+ pub param_filter: Vec<ParamFilter>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum TimeOrText {
+ Time(TimeRange),
+ Text(TextMatch),
+}
+
+/// Name: text-match
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a substring match on a property or parameter
+/// value.
+///
+/// Description: The CALDAV:text-match XML element specifies text used
+/// for a substring match against the property or parameter value
+/// specified in a calendaring REPORT request.
+///
+/// The "collation" attribute is used to select the collation that the
+/// server MUST use for character string matching. In the absence of
+/// this attribute, the server MUST use the "i;ascii-casemap"
+/// collation.
+///
+/// The "negate-condition" attribute is used to indicate that this
+/// test returns a match if the text matches when the attribute value
+/// is set to "no", or return a match if the text does not match, if
+/// the attribute value is set to "yes". For example, this can be
+/// used to match components with a STATUS property not set to
+/// CANCELLED.
+///
+/// Definition:
+/// <!ELEMENT text-match (#PCDATA)>
+/// PCDATA value: string
+/// <!ATTLIST text-match collation CDATA "i;ascii-casemap"
+/// negate-condition (yes | no) "no">
+#[derive(Debug, PartialEq, Clone)]
+pub struct TextMatch {
+ pub collation: Option<Collation>,
+ pub negate_condition: Option<bool>,
+ pub text: String,
+}
+
+/// Name: param-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Limits the search to specific parameter values.
+///
+/// Description: The CALDAV:param-filter XML element specifies a query
+/// targeted at a specific calendar property parameter (e.g.,
+/// PARTSTAT) in the scope of the calendar property on which it is
+/// defined. A calendar property parameter is said to match a CALDAV:
+/// param-filter if:
+///
+/// * The CALDAV:param-filter XML element is empty and a parameter of
+/// the type specified by the "name" attribute exists on the
+/// calendar property being examined;
+///
+/// or:
+///
+/// * The CALDAV:param-filter XML element contains a CALDAV:is-not-
+/// defined XML element and no parameter of the type specified by
+/// the "name" attribute exists on the calendar property being
+/// examined;
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT param-filter (is-not-defined | text-match?)>
+///
+/// <!ATTLIST param-filter name CDATA #REQUIRED>
+/// name value: a property parameter name (e.g., PARTSTAT)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct ParamFilter {
+ pub name: PropertyParameter,
+ pub additional_rules: Option<ParamFilterMatch>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum ParamFilterMatch {
+ IsNotDefined,
+ Match(TextMatch),
+}
+
+/// CALDAV:is-not-defined XML Element
+///
+/// Name: is-not-defined
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that a match should occur if the enclosing
+/// component, property, or parameter does not exist.
+///
+/// Description: The CALDAV:is-not-defined XML element specifies that a
+/// match occurs if the enclosing component, property, or parameter
+/// value specified in a calendaring REPORT request does not exist in
+/// the calendar data being tested.
+///
+/// Definition:
+/// <!ELEMENT is-not-defined EMPTY>
+/* CURRENTLY INLINED */
+
+/// Name: timezone
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies the time zone component to use when determining
+/// the results of a report.
+///
+/// Description: The CALDAV:timezone XML element specifies that for a
+/// given calendaring REPORT request, the server MUST rely on the
+/// specified VTIMEZONE component instead of the CALDAV:calendar-
+/// timezone property of the calendar collection, in which the
+/// calendar object resource is contained to resolve "date" values and
+/// "date with local time" values (i.e., floating time) to "date with
+/// UTC time" values. The server will require this information to
+/// determine if a calendar component scheduled with "date" values or
+/// "date with local time" values intersects a CALDAV:time-range
+/// specified in a CALDAV:calendar-query REPORT.
+///
+/// Note: The iCalendar data embedded within the CALDAV:timezone XML
+/// element MUST follow the standard XML character data encoding
+/// rules, including use of &lt;, &gt;, &amp; etc. entity encoding or
+/// the use of a <![CDATA[ ... ]]> construct. In the later case, the
+///
+/// iCalendar data cannot contain the character sequence "]]>", which
+/// is the end delimiter for the CDATA section.
+///
+/// Definition:
+///
+/// <!ELEMENT timezone (#PCDATA)>
+/// PCDATA value: an iCalendar object with exactly one VTIMEZONE
+#[derive(Debug, PartialEq, Clone)]
+pub struct TimeZone(pub String);
+
+/// Name: filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a filter to limit the set of calendar components
+/// returned by the server.
+///
+/// Description: The CALDAV:filter XML element specifies the search
+/// filter used to limit the calendar components returned by a
+/// calendaring REPORT request.
+///
+/// Definition:
+/// <!ELEMENT filter (comp-filter)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Filter(pub CompFilter);
+
+/// Name: time-range
+///
+/// Definition:
+///
+/// <!ELEMENT time-range EMPTY>
+/// <!ATTLIST time-range start CDATA #IMPLIED
+/// end CDATA #IMPLIED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub enum TimeRange {
+ OnlyStart(DateTime<Utc>),
+ OnlyEnd(DateTime<Utc>),
+ FullRange(DateTime<Utc>, DateTime<Utc>),
+}
+
+// ----------------------- ENUM ATTRIBUTES ---------------------
+
+/// Known components
+#[derive(Debug, PartialEq, Clone)]
+pub enum Component {
+ VCalendar,
+ VJournal,
+ VFreeBusy,
+ VEvent,
+ VTodo,
+ VAlarm,
+ VTimeZone,
+ Unknown(String),
+}
+impl Component {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ match self {
+ Self::VCalendar => "VCALENDAR",
+ Self::VJournal => "VJOURNAL",
+ Self::VFreeBusy => "VFREEBUSY",
+ Self::VEvent => "VEVENT",
+ Self::VTodo => "VTODO",
+ Self::VAlarm => "VALARM",
+ Self::VTimeZone => "VTIMEZONE",
+ Self::Unknown(c) => c,
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "VCALENDAR" => Self::VCalendar,
+ "VJOURNAL" => Self::VJournal,
+ "VFREEBUSY" => Self::VFreeBusy,
+ "VEVENT" => Self::VEvent,
+ "VTODO" => Self::VTodo,
+ "VALARM" => Self::VAlarm,
+ "VTIMEZONE" => Self::VTimeZone,
+ _ => Self::Unknown(v),
+ }
+ }
+}
+
+/// name="VERSION", name="SUMMARY", etc.
+/// Can be set on different objects: VCalendar, VEvent, etc.
+/// Might be replaced by an enum later
+#[derive(Debug, PartialEq, Clone)]
+pub struct ComponentProperty(pub String);
+
+/// like PARSTAT
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropertyParameter(pub String);
+impl PropertyParameter {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ self.0.as_str()
+ }
+}
+
+#[derive(Default, Debug, PartialEq, Clone)]
+pub enum Collation {
+ #[default]
+ AsciiCaseMap,
+ Octet,
+ Unknown(String),
+}
+impl Collation {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ match self {
+ Self::AsciiCaseMap => "i;ascii-casemap",
+ Self::Octet => "i;octet",
+ Self::Unknown(c) => c.as_str(),
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "i;ascii-casemap" => Self::AsciiCaseMap,
+ "i;octet" => Self::Octet,
+ _ => Self::Unknown(v),
+ }
+ }
+}
diff --git a/aero-dav/src/decoder.rs b/aero-dav/src/decoder.rs
new file mode 100644
index 0000000..bb64455
--- /dev/null
+++ b/aero-dav/src/decoder.rs
@@ -0,0 +1,1152 @@
+use chrono::DateTime;
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::types::*;
+use super::xml::{IRead, Node, QRead, Reader, DAV_URN};
+
+//@TODO (1) Rewrite all objects as Href,
+// where we return Ok(None) instead of trying to find the object at any cost.
+// Add a xml.find<E: Qread>() -> Result<Option<E>, ParsingError> or similar for the cases we
+// really need the object
+// (2) Rewrite QRead and replace Result<Option<_>, _> with Result<_, _>, not found being a possible
+// error.
+// (3) Rewrite vectors with xml.collect<E: QRead>() -> Result<Vec<E>, _>
+// (4) Something for alternatives like xml::choices on some lib would be great but no idea yet
+
+// ---- ROOT ----
+
+/// Propfind request
+impl<E: Extension> QRead<PropFind<E>> for PropFind<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propfind").await?;
+ let propfind: PropFind<E> = loop {
+ // allprop
+ if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
+ xml.close().await?;
+ let includ = xml.maybe_find::<Include<E>>().await?;
+ break PropFind::AllProp(includ);
+ }
+
+ // propname
+ if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
+ xml.close().await?;
+ break PropFind::PropName;
+ }
+
+ // prop
+ let (mut maybe_prop, mut dirty) = (None, false);
+ xml.maybe_read::<PropName<E>>(&mut maybe_prop, &mut dirty)
+ .await?;
+ if let Some(prop) = maybe_prop {
+ break PropFind::Prop(prop);
+ }
+
+ // not found, skipping
+ xml.skip().await?;
+ };
+ xml.close().await?;
+
+ Ok(propfind)
+ }
+}
+
+/// PROPPATCH request
+impl<E: Extension> QRead<PropertyUpdate<E>> for PropertyUpdate<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propertyupdate").await?;
+ let collected_items = xml.collect::<PropertyUpdateItem<E>>().await?;
+ xml.close().await?;
+ Ok(PropertyUpdate(collected_items))
+ }
+}
+
+/// Generic response
+impl<E: Extension> QRead<Multistatus<E>> for Multistatus<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "multistatus").await?;
+ let mut responses = Vec::new();
+ let mut responsedescription = None;
+ let mut extension = None;
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut responses, &mut dirty).await?;
+ xml.maybe_read(&mut responsedescription, &mut dirty).await?;
+ xml.maybe_read(&mut extension, &mut dirty).await?;
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Multistatus {
+ responses,
+ responsedescription,
+ extension,
+ })
+ }
+}
+
+// LOCK REQUEST
+impl QRead<LockInfo> for LockInfo {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockinfo").await?;
+ let (mut m_scope, mut m_type, mut owner) = (None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
+ xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+ match (m_scope, m_type) {
+ (Some(lockscope), Some(locktype)) => Ok(LockInfo {
+ lockscope,
+ locktype,
+ owner,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+// LOCK RESPONSE
+impl<E: Extension> QRead<PropValue<E>> for PropValue<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ println!("---- propvalue");
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<Property<E>>().await?;
+ xml.close().await?;
+ Ok(PropValue(acc))
+ }
+}
+
+/// Error response
+impl<E: Extension> QRead<Error<E>> for Error<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "error").await?;
+ let violations = xml.collect::<Violation<E>>().await?;
+ xml.close().await?;
+ Ok(Error(violations))
+ }
+}
+
+// ---- INNER XML
+impl<E: Extension> QRead<Response<E>> for Response<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "response").await?;
+ let (mut status, mut error, mut responsedescription, mut location) =
+ (None, None, None, None);
+ let mut href = Vec::new();
+ let mut propstat = Vec::new();
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<Status>(&mut status, &mut dirty).await?;
+ xml.maybe_push::<Href>(&mut href, &mut dirty).await?;
+ xml.maybe_push::<PropStat<E>>(&mut propstat, &mut dirty)
+ .await?;
+ xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
+ xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty)
+ .await?;
+ xml.maybe_read::<Location>(&mut location, &mut dirty)
+ .await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (status, &propstat[..], &href[..]) {
+ (Some(status), &[], &[_, ..]) => Ok(Response {
+ status_or_propstat: StatusOrPropstat::Status(href, status),
+ error,
+ responsedescription,
+ location,
+ }),
+ (None, &[_, ..], &[_, ..]) => Ok(Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ href.into_iter().next().unwrap(),
+ propstat,
+ ),
+ error,
+ responsedescription,
+ location,
+ }),
+ (Some(_), &[_, ..], _) => Err(ParsingError::InvalidValue),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl<E: Extension> QRead<PropStat<E>> for PropStat<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propstat").await?;
+
+ let (mut m_any_prop, mut m_status, mut error, mut responsedescription) =
+ (None, None, None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<AnyProp<E>>(&mut m_any_prop, &mut dirty)
+ .await?;
+ xml.maybe_read::<Status>(&mut m_status, &mut dirty).await?;
+ xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
+ xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty)
+ .await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (m_any_prop, m_status) {
+ (Some(prop), Some(status)) => Ok(PropStat {
+ prop,
+ status,
+ error,
+ responsedescription,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<Status> for Status {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "status").await?;
+ let fullcode = xml.tag_string().await?;
+ let txtcode = fullcode
+ .splitn(3, ' ')
+ .nth(1)
+ .ok_or(ParsingError::InvalidValue)?;
+ let code = http::status::StatusCode::from_bytes(txtcode.as_bytes())
+ .or(Err(ParsingError::InvalidValue))?;
+ xml.close().await?;
+ Ok(Status(code))
+ }
+}
+
+impl QRead<ResponseDescription> for ResponseDescription {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "responsedescription").await?;
+ let cnt = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(ResponseDescription(cnt))
+ }
+}
+
+impl QRead<Location> for Location {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "location").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(Location(href))
+ }
+}
+
+impl<E: Extension> QRead<PropertyUpdateItem<E>> for PropertyUpdateItem<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Remove::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyUpdateItem::Remove),
+ }
+ Set::qread(xml).await.map(PropertyUpdateItem::Set)
+ }
+}
+
+impl<E: Extension> QRead<Remove<E>> for Remove<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "remove").await?;
+ let propname = xml.find::<PropName<E>>().await?;
+ xml.close().await?;
+ Ok(Remove(propname))
+ }
+}
+
+impl<E: Extension> QRead<Set<E>> for Set<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "set").await?;
+ let propvalue = xml.find::<PropValue<E>>().await?;
+ xml.close().await?;
+ Ok(Set(propvalue))
+ }
+}
+
+impl<E: Extension> QRead<Violation<E>> for Violation<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "lock-token-matches-request-uri")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::LockTokenMatchesRequestUri)
+ } else if xml
+ .maybe_open(DAV_URN, "lock-token-submitted")
+ .await?
+ .is_some()
+ {
+ let links = xml.collect::<Href>().await?;
+ xml.close().await?;
+ Ok(Violation::LockTokenSubmitted(links))
+ } else if xml
+ .maybe_open(DAV_URN, "no-conflicting-lock")
+ .await?
+ .is_some()
+ {
+ let links = xml.collect::<Href>().await?;
+ xml.close().await?;
+ Ok(Violation::NoConflictingLock(links))
+ } else if xml
+ .maybe_open(DAV_URN, "no-external-entities")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::NoExternalEntities)
+ } else if xml
+ .maybe_open(DAV_URN, "preserved-live-properties")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::PreservedLiveProperties)
+ } else if xml
+ .maybe_open(DAV_URN, "propfind-finite-depth")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::PropfindFiniteDepth)
+ } else if xml
+ .maybe_open(DAV_URN, "cannot-modify-protected-property")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::CannotModifyProtectedProperty)
+ } else {
+ E::Error::qread(xml).await.map(Violation::Extension)
+ }
+ }
+}
+
+impl<E: Extension> QRead<Include<E>> for Include<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "include").await?;
+ let acc = xml.collect::<PropertyRequest<E>>().await?;
+ xml.close().await?;
+ Ok(Include(acc))
+ }
+}
+
+impl<E: Extension> QRead<PropName<E>> for PropName<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<PropertyRequest<E>>().await?;
+ xml.close().await?;
+ Ok(PropName(acc))
+ }
+}
+
+impl<E: Extension> QRead<AnyProp<E>> for AnyProp<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<AnyProperty<E>>().await?;
+ xml.close().await?;
+ Ok(AnyProp(acc))
+ }
+}
+
+impl<E: Extension> QRead<AnyProperty<E>> for AnyProperty<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Property::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Value),
+ }
+ PropertyRequest::qread(xml).await.map(Self::Request)
+ }
+}
+
+impl<E: Extension> QRead<PropertyRequest<E>> for PropertyRequest<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let maybe = if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() {
+ Some(PropertyRequest::CreationDate)
+ } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() {
+ Some(PropertyRequest::DisplayName)
+ } else if xml
+ .maybe_open(DAV_URN, "getcontentlanguage")
+ .await?
+ .is_some()
+ {
+ Some(PropertyRequest::GetContentLanguage)
+ } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() {
+ Some(PropertyRequest::GetContentLength)
+ } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() {
+ Some(PropertyRequest::GetContentType)
+ } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() {
+ Some(PropertyRequest::GetEtag)
+ } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() {
+ Some(PropertyRequest::GetLastModified)
+ } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() {
+ Some(PropertyRequest::LockDiscovery)
+ } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() {
+ Some(PropertyRequest::ResourceType)
+ } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() {
+ Some(PropertyRequest::SupportedLock)
+ } else {
+ None
+ };
+
+ match maybe {
+ Some(pr) => {
+ xml.close().await?;
+ Ok(pr)
+ }
+ None => E::PropertyRequest::qread(xml)
+ .await
+ .map(PropertyRequest::Extension),
+ }
+ }
+}
+
+impl<E: Extension> QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ // Core WebDAV properties
+ if xml
+ .maybe_open_start(DAV_URN, "creationdate")
+ .await?
+ .is_some()
+ {
+ let datestr = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CreationDate(DateTime::parse_from_rfc3339(
+ datestr.as_str(),
+ )?));
+ } else if xml
+ .maybe_open_start(DAV_URN, "displayname")
+ .await?
+ .is_some()
+ {
+ let name = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::DisplayName(name));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontentlanguage")
+ .await?
+ .is_some()
+ {
+ let lang = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetContentLanguage(lang));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontentlength")
+ .await?
+ .is_some()
+ {
+ let cl = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::GetContentLength(cl));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontenttype")
+ .await?
+ .is_some()
+ {
+ let ct = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetContentType(ct));
+ } else if xml.maybe_open_start(DAV_URN, "getetag").await?.is_some() {
+ let etag = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetEtag(etag));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getlastmodified")
+ .await?
+ .is_some()
+ {
+ let datestr = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetLastModified(DateTime::parse_from_rfc2822(
+ datestr.as_str(),
+ )?));
+ } else if xml
+ .maybe_open_start(DAV_URN, "lockdiscovery")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<ActiveLock>().await?;
+ xml.close().await?;
+ return Ok(Property::LockDiscovery(acc));
+ } else if xml
+ .maybe_open_start(DAV_URN, "resourcetype")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<ResourceType<E>>().await?;
+ xml.close().await?;
+ return Ok(Property::ResourceType(acc));
+ } else if xml
+ .maybe_open_start(DAV_URN, "supportedlock")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<LockEntry>().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedLock(acc));
+ }
+
+ // Option 2: an extension property, delegating
+ E::Property::qread(xml).await.map(Property::Extension)
+ }
+}
+
+impl QRead<ActiveLock> for ActiveLock {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "activelock").await?;
+ let (
+ mut m_scope,
+ mut m_type,
+ mut m_depth,
+ mut owner,
+ mut timeout,
+ mut locktoken,
+ mut m_root,
+ ) = (None, None, None, None, None, None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
+ xml.maybe_read::<Depth>(&mut m_depth, &mut dirty).await?;
+ xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
+ xml.maybe_read::<Timeout>(&mut timeout, &mut dirty).await?;
+ xml.maybe_read::<LockToken>(&mut locktoken, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockRoot>(&mut m_root, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => {
+ xml.skip().await?;
+ }
+ }
+ }
+ }
+
+ xml.close().await?;
+ match (m_scope, m_type, m_depth, m_root) {
+ (Some(lockscope), Some(locktype), Some(depth), Some(lockroot)) => Ok(ActiveLock {
+ lockscope,
+ locktype,
+ depth,
+ owner,
+ timeout,
+ locktoken,
+ lockroot,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<Depth> for Depth {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "depth").await?;
+ let depth_str = xml.tag_string().await?;
+ xml.close().await?;
+ match depth_str.as_str() {
+ "0" => Ok(Depth::Zero),
+ "1" => Ok(Depth::One),
+ "infinity" => Ok(Depth::Infinity),
+ _ => Err(ParsingError::WrongToken),
+ }
+ }
+}
+
+impl QRead<Owner> for Owner {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "owner").await?;
+
+ let mut owner = Owner::Unknown;
+ loop {
+ match xml.peek() {
+ Event::Text(_) | Event::CData(_) => {
+ let txt = xml.tag_string().await?;
+ if matches!(owner, Owner::Unknown) {
+ owner = Owner::Txt(txt);
+ }
+ }
+ Event::Start(_) | Event::Empty(_) => match Href::qread(xml).await {
+ Ok(href) => {
+ owner = Owner::Href(href);
+ }
+ Err(ParsingError::Recoverable) => {
+ xml.skip().await?;
+ }
+ Err(e) => return Err(e),
+ },
+ Event::End(_) => break,
+ _ => {
+ xml.skip().await?;
+ }
+ }
+ }
+ xml.close().await?;
+ Ok(owner)
+ }
+}
+
+impl QRead<Timeout> for Timeout {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ const SEC_PFX: &str = "Second-";
+ xml.open(DAV_URN, "timeout").await?;
+
+ let timeout = match xml.tag_string().await?.as_str() {
+ "Infinite" => Timeout::Infinite,
+ seconds => match seconds.strip_prefix(SEC_PFX) {
+ Some(secs) => Timeout::Seconds(secs.parse::<u32>()?),
+ None => return Err(ParsingError::InvalidValue),
+ },
+ };
+
+ xml.close().await?;
+ Ok(timeout)
+ }
+}
+
+impl QRead<LockToken> for LockToken {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "locktoken").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(LockToken(href))
+ }
+}
+
+impl QRead<LockRoot> for LockRoot {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockroot").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(LockRoot(href))
+ }
+}
+
+impl<E: Extension> QRead<ResourceType<E>> for ResourceType<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "collection").await?.is_some() {
+ xml.close().await?;
+ return Ok(ResourceType::Collection);
+ }
+
+ E::ResourceType::qread(xml)
+ .await
+ .map(ResourceType::Extension)
+ }
+}
+
+impl QRead<LockEntry> for LockEntry {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockentry").await?;
+ let (mut maybe_scope, mut maybe_type) = (None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut maybe_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut maybe_type, &mut dirty)
+ .await?;
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (maybe_scope, maybe_type) {
+ (Some(lockscope), Some(locktype)) => Ok(LockEntry {
+ lockscope,
+ locktype,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<LockScope> for LockScope {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockscope").await?;
+
+ let lockscope = loop {
+ if xml.maybe_open(DAV_URN, "exclusive").await?.is_some() {
+ xml.close().await?;
+ break LockScope::Exclusive;
+ }
+
+ if xml.maybe_open(DAV_URN, "shared").await?.is_some() {
+ xml.close().await?;
+ break LockScope::Shared;
+ }
+
+ xml.skip().await?;
+ };
+
+ xml.close().await?;
+ Ok(lockscope)
+ }
+}
+
+impl QRead<LockType> for LockType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "locktype").await?;
+
+ let locktype = loop {
+ if xml.maybe_open(DAV_URN, "write").await?.is_some() {
+ xml.close().await?;
+ break LockType::Write;
+ }
+
+ xml.skip().await?;
+ };
+
+ xml.close().await?;
+ Ok(locktype)
+ }
+}
+
+impl QRead<Href> for Href {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "href").await?;
+ let url = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Href(url))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Core;
+ use chrono::{FixedOffset, TimeZone};
+ use quick_xml::reader::NsReader;
+
+ #[tokio::test]
+ async fn basic_propfind_propname() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+<rando/>
+<garbage><old/></garbage>
+<D:propfind xmlns:D="DAV:">
+ <D:propname/>
+</D:propfind>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropFind<Core>>().await.unwrap();
+
+ assert_eq!(got, PropFind::<Core>::PropName);
+ }
+
+ #[tokio::test]
+ async fn basic_propfind_prop() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+<rando/>
+<garbage><old/></garbage>
+<D:propfind xmlns:D="DAV:">
+ <D:prop>
+ <D:displayname/>
+ <D:getcontentlength/>
+ <D:getcontenttype/>
+ <D:getetag/>
+ <D:getlastmodified/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+</D:propfind>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropFind<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ PropFind::Prop(PropName(vec![
+ PropertyRequest::DisplayName,
+ PropertyRequest::GetContentLength,
+ PropertyRequest::GetContentType,
+ PropertyRequest::GetEtag,
+ PropertyRequest::GetLastModified,
+ PropertyRequest::ResourceType,
+ PropertyRequest::SupportedLock,
+ ]))
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_lock_error() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:error xmlns:D="DAV:">
+ <D:lock-token-submitted>
+ <D:href>/locked/</D:href>
+ </D:lock-token-submitted>
+ </D:error>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Error<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Error(vec![Violation::LockTokenSubmitted(vec![Href(
+ "/locked/".into()
+ )])])
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_propertyupdate() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:propertyupdate xmlns:D="DAV:"
+ xmlns:Z="http://ns.example.com/standards/z39.50/">
+ <D:set>
+ <D:prop>
+ <Z:Authors>
+ <Z:Author>Jim Whitehead</Z:Author>
+ <Z:Author>Roy Fielding</Z:Author>
+ </Z:Authors>
+ </D:prop>
+ </D:set>
+ <D:remove>
+ <D:prop><Z:Copyright-Owner/></D:prop>
+ </D:remove>
+ </D:propertyupdate>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropertyUpdate<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ PropertyUpdate(vec![
+ PropertyUpdateItem::Set(Set(PropValue(vec![]))),
+ PropertyUpdateItem::Remove(Remove(PropName(vec![]))),
+ ])
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_lockinfo() {
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<D:lockinfo xmlns:D='DAV:'>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+</D:lockinfo>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<LockInfo>().await.unwrap();
+
+ assert_eq!(
+ got,
+ LockInfo {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into()
+ ))),
+ }
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_multistatus_name() {
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+ <multistatus xmlns="DAV:">
+ <response>
+ <href>http://www.example.com/container/</href>
+ <propstat>
+ <prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox/>
+ <R:author/>
+ <creationdate/>
+ <displayname/>
+ <resourcetype/>
+ <supportedlock/>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ <response>
+ <href>http://www.example.com/container/front.html</href>
+ <propstat>
+ <prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox/>
+ <creationdate/>
+ <displayname/>
+ <getcontentlength/>
+ <getcontenttype/>
+ <getetag/>
+ <getlastmodified/>
+ <resourcetype/>
+ <supportedlock/>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ </multistatus>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Multistatus<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ extension: None,
+ }
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_multistatus_value() {
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/container/</D:href>
+ <D:propstat>
+ <D:prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox><R:BoxType>Box type A</R:BoxType></R:bigbox>
+ <R:author><R:Name>Hadrian</R:Name></R:author>
+ <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ <D:displayname>Example collection</D:displayname>
+ <D:resourcetype><D:collection/></D:resourcetype>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope><D:shared/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/container/front.html</D:href>
+ <D:propstat>
+ <D:prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox><R:BoxType>Box type B</R:BoxType>
+ </R:bigbox>
+ <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate>
+ <D:displayname>Example HTML resource</D:displayname>
+ <D:getcontentlength>4525</D:getcontentlength>
+ <D:getcontenttype>text/html</D:getcontenttype>
+ <D:getetag>"zzyzx"</D:getetag>
+ <D:getlastmodified
+ >Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified>
+ <D:resourcetype/>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope><D:shared/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Multistatus<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 17, 42, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into()
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection
+ ])),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 18, 27, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into()
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType(
+ "text/html".into()
+ )),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::west_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 01, 12, 09, 25, 56)
+ .unwrap()
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ }
+ );
+ }
+}
diff --git a/aero-dav/src/encoder.rs b/aero-dav/src/encoder.rs
new file mode 100644
index 0000000..6c77aa6
--- /dev/null
+++ b/aero-dav/src/encoder.rs
@@ -0,0 +1,1262 @@
+use super::types::*;
+use super::xml::{IWrite, Node, QWrite, Writer};
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+// --- XML ROOTS
+
+/// PROPFIND REQUEST
+impl<E: Extension> QWrite for PropFind<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propfind");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::PropName => {
+ let empty_propname = xml.create_dav_element("propname");
+ xml.q
+ .write_event_async(Event::Empty(empty_propname))
+ .await?
+ }
+ Self::AllProp(maybe_include) => {
+ let empty_allprop = xml.create_dav_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_allprop)).await?;
+ if let Some(include) = maybe_include {
+ include.qwrite(xml).await?;
+ }
+ }
+ Self::Prop(propname) => propname.qwrite(xml).await?,
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// PROPPATCH REQUEST
+impl<E: Extension> QWrite for PropertyUpdate<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propertyupdate");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for update in self.0.iter() {
+ update.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// PROPFIND RESPONSE, PROPPATCH RESPONSE, COPY RESPONSE, MOVE RESPONSE
+/// DELETE RESPONSE,
+impl<E: Extension> QWrite for Multistatus<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("multistatus");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for response in self.responses.iter() {
+ response.qwrite(xml).await?;
+ }
+ if let Some(description) = &self.responsedescription {
+ description.qwrite(xml).await?;
+ }
+ if let Some(extension) = &self.extension {
+ extension.qwrite(xml).await?;
+ }
+
+ xml.q.write_event_async(Event::End(end)).await?;
+ Ok(())
+ }
+}
+
+/// LOCK REQUEST
+impl QWrite for LockInfo {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockinfo");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.locktype.qwrite(xml).await?;
+ if let Some(owner) = &self.owner {
+ owner.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// SOME LOCK RESPONSES
+impl<E: Extension> QWrite for PropValue<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propval in &self.0 {
+ propval.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// Error response
+impl<E: Extension> QWrite for Error<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("error");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for violation in &self.0 {
+ violation.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// --- XML inner elements
+impl<E: Extension> QWrite for PropertyUpdateItem<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Set(set) => set.qwrite(xml).await,
+ Self::Remove(rm) => rm.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Set<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("set");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Remove<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("remove");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropName<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propname in &self.0 {
+ propname.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for AnyProp<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propname in &self.0 {
+ propname.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for AnyProperty<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Request(v) => v.qwrite(xml).await,
+ Self::Value(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for Href {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("href");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&self.0)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Response<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("response");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.status_or_propstat.qwrite(xml).await?;
+ if let Some(error) = &self.error {
+ error.qwrite(xml).await?;
+ }
+ if let Some(responsedescription) = &self.responsedescription {
+ responsedescription.qwrite(xml).await?;
+ }
+ if let Some(location) = &self.location {
+ location.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for StatusOrPropstat<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Status(many_href, status) => {
+ for href in many_href.iter() {
+ href.qwrite(xml).await?;
+ }
+ status.qwrite(xml).await
+ }
+ Self::PropStat(href, propstat_list) => {
+ href.qwrite(xml).await?;
+ for propstat in propstat_list.iter() {
+ propstat.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for Status {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("status");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+
+ let txt = format!(
+ "HTTP/1.1 {} {}",
+ self.0.as_str(),
+ self.0.canonical_reason().unwrap_or("No reason")
+ );
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?;
+
+ xml.q.write_event_async(Event::End(end)).await?;
+
+ Ok(())
+ }
+}
+
+impl QWrite for ResponseDescription {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("responsedescription");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&self.0)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Location {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("location");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropStat<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propstat");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.prop.qwrite(xml).await?;
+ self.status.qwrite(xml).await?;
+ if let Some(error) = &self.error {
+ error.qwrite(xml).await?;
+ }
+ if let Some(description) = &self.responsedescription {
+ description.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+
+ Ok(())
+ }
+}
+
+impl<E: Extension> QWrite for Property<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ use Property::*;
+ match self {
+ CreationDate(date) => {
+ // <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ let start = xml.create_dav_element("creationdate");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&date.to_rfc3339())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ DisplayName(name) => {
+ // <D:displayname>Example collection</D:displayname>
+ let start = xml.create_dav_element("displayname");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(name)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentLanguage(lang) => {
+ let start = xml.create_dav_element("getcontentlanguage");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(lang)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentLength(len) => {
+ // <D:getcontentlength>4525</D:getcontentlength>
+ let start = xml.create_dav_element("getcontentlength");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&len.to_string())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentType(ct) => {
+ // <D:getcontenttype>text/html</D:getcontenttype>
+ let start = xml.create_dav_element("getcontenttype");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&ct)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetEtag(et) => {
+ // <D:getetag>"zzyzx"</D:getetag>
+ let start = xml.create_dav_element("getetag");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(et)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetLastModified(date) => {
+ // <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified>
+ let start = xml.create_dav_element("getlastmodified");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&date.to_rfc2822())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ LockDiscovery(many_locks) => {
+ // <D:lockdiscovery><D:activelock> ... </D:activelock></D:lockdiscovery>
+ let start = xml.create_dav_element("lockdiscovery");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for lock in many_locks.iter() {
+ lock.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ ResourceType(many_types) => {
+ // <D:resourcetype><D:collection/></D:resourcetype>
+
+ // <D:resourcetype/>
+
+ // <x:resourcetype xmlns:x="DAV:">
+ // <x:collection/>
+ // <f:search-results xmlns:f="http://www.example.com/ns"/>
+ // </x:resourcetype>
+
+ let start = xml.create_dav_element("resourcetype");
+ if many_types.is_empty() {
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ } else {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for restype in many_types.iter() {
+ restype.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ }
+ SupportedLock(many_entries) => {
+ // <D:supportedlock/>
+
+ // <D:supportedlock> <D:lockentry> ... </D:lockentry> </D:supportedlock>
+
+ let start = xml.create_dav_element("supportedlock");
+ if many_entries.is_empty() {
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ } else {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for entry in many_entries.iter() {
+ entry.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ }
+ Extension(inner) => inner.qwrite(xml).await?,
+ };
+ Ok(())
+ }
+}
+
+impl<E: Extension> QWrite for ResourceType<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Collection => {
+ let empty_collection = xml.create_dav_element("collection");
+ xml.q
+ .write_event_async(Event::Empty(empty_collection))
+ .await
+ }
+ Self::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Include<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("include");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for prop in self.0.iter() {
+ prop.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropertyRequest<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ use PropertyRequest::*;
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ CreationDate => atom("creationdate").await,
+ DisplayName => atom("displayname").await,
+ GetContentLanguage => atom("getcontentlanguage").await,
+ GetContentLength => atom("getcontentlength").await,
+ GetContentType => atom("getcontenttype").await,
+ GetEtag => atom("getetag").await,
+ GetLastModified => atom("getlastmodified").await,
+ LockDiscovery => atom("lockdiscovery").await,
+ ResourceType => atom("resourcetype").await,
+ SupportedLock => atom("supportedlock").await,
+ Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for ActiveLock {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ // <D:activelock>
+ // <D:locktype><D:write/></D:locktype>
+ // <D:lockscope><D:exclusive/></D:lockscope>
+ // <D:depth>infinity</D:depth>
+ // <D:owner>
+ // <D:href>http://example.org/~ejw/contact.html</D:href>
+ // </D:owner>
+ // <D:timeout>Second-604800</D:timeout>
+ // <D:locktoken>
+ // <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href>
+ // </D:locktoken>
+ // <D:lockroot>
+ // <D:href>http://example.com/workspace/webdav/proposal.doc</D:href>
+ // </D:lockroot>
+ // </D:activelock>
+ let start = xml.create_dav_element("activelock");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.locktype.qwrite(xml).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.depth.qwrite(xml).await?;
+ if let Some(owner) = &self.owner {
+ owner.qwrite(xml).await?;
+ }
+ if let Some(timeout) = &self.timeout {
+ timeout.qwrite(xml).await?;
+ }
+ if let Some(locktoken) = &self.locktoken {
+ locktoken.qwrite(xml).await?;
+ }
+ self.lockroot.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("locktype");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Write => {
+ let empty_write = xml.create_dav_element("write");
+ xml.q.write_event_async(Event::Empty(empty_write)).await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockScope {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockscope");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Exclusive => {
+ let empty_tag = xml.create_dav_element("exclusive");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await?
+ }
+ Self::Shared => {
+ let empty_tag = xml.create_dav_element("shared");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Owner {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("owner");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Txt(txt) => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?
+ }
+ Self::Href(href) => href.qwrite(xml).await?,
+ Self::Unknown => (),
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Depth {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("depth");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Zero => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("0")))
+ .await?
+ }
+ Self::One => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("1")))
+ .await?
+ }
+ Self::Infinity => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("infinity")))
+ .await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Timeout {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("timeout");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Seconds(count) => {
+ let txt = format!("Second-{}", count);
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?
+ }
+ Self::Infinite => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("Infinite")))
+ .await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockToken {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("locktoken");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockRoot {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockroot");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockEntry {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockentry");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.locktype.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Violation<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Violation::LockTokenMatchesRequestUri => atom("lock-token-matches-request-uri").await,
+ Violation::LockTokenSubmitted(hrefs) if hrefs.is_empty() => {
+ atom("lock-token-submitted").await
+ }
+ Violation::LockTokenSubmitted(hrefs) => {
+ let start = xml.create_dav_element("lock-token-submitted");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for href in hrefs {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Violation::NoConflictingLock(hrefs) if hrefs.is_empty() => {
+ atom("no-conflicting-lock").await
+ }
+ Violation::NoConflictingLock(hrefs) => {
+ let start = xml.create_dav_element("no-conflicting-lock");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for href in hrefs {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Violation::NoExternalEntities => atom("no-external-entities").await,
+ Violation::PreservedLiveProperties => atom("preserved-live-properties").await,
+ Violation::PropfindFiniteDepth => atom("propfind-finite-depth").await,
+ Violation::CannotModifyProtectedProperty => {
+ atom("cannot-modify-protected-property").await
+ }
+ Violation::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::super::xml;
+ use super::*;
+ use crate::realization::Core;
+ use tokio::io::AsyncWriteExt;
+
+ /// To run only the unit tests and avoid the behavior ones:
+ /// cargo test --bin aerogramme
+
+ async fn serialize(elem: &impl QWrite) -> String {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![("xmlns:D".into(), "DAV:".into())];
+ let mut writer = Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ return got.into();
+ }
+
+ async fn deserialize<T: xml::Node<T>>(src: &str) -> T {
+ let mut rdr = xml::Reader::new(quick_xml::reader::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn basic_href() {
+ let orig = Href("/SOGo/dav/so/".into());
+
+ let got = serialize(&orig).await;
+ let expected = r#"<D:href xmlns:D="DAV:">/SOGo/dav/so/</D:href>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Href>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn basic_multistatus() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![],
+ responsedescription: Some(ResponseDescription("Hello world".into())),
+ };
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:responsedescription>Hello world</D:responsedescription>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_error_delete_locked() {
+ let orig = Error::<Core>(vec![Violation::LockTokenSubmitted(vec![Href(
+ "/locked/".into(),
+ )])]);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:error xmlns:D="DAV:">
+ <D:lock-token-submitted>
+ <D:href>/locked/</D:href>
+ </D:lock-token-submitted>
+</D:error>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Error<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_req() {
+ let orig = PropFind::<Core>::PropName;
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:propname/>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_res() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>http://www.example.com/container/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate/>
+ <D:displayname/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://www.example.com/container/front.html</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate/>
+ <D:displayname/>
+ <D:getcontentlength/>
+ <D:getcontenttype/>
+ <D:getetag/>
+ <D:getlastmodified/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_req() {
+ let orig = PropFind::<Core>::AllProp(None);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:allprop/>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_res() {
+ use chrono::{FixedOffset, TimeZone};
+
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 17, 42, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into(),
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection,
+ ])),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 18, 27, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into(),
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType("text/html".into())),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::east_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 1, 12, 9, 25, 56)
+ .unwrap(),
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/container/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ <D:displayname>Example collection</D:displayname>
+ <D:resourcetype>
+ <D:collection/>
+ </D:resourcetype>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope>
+ <D:shared/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/container/front.html</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate>
+ <D:displayname>Example HTML resource</D:displayname>
+ <D:getcontentlength>4525</D:getcontentlength>
+ <D:getcontenttype>text/html</D:getcontenttype>
+ <D:getetag>&quot;zzyzx&quot;</D:getetag>
+ <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 +0000</D:getlastmodified>
+ <D:resourcetype/>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope>
+ <D:shared/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_include() {
+ let orig = PropFind::<Core>::AllProp(Some(Include(vec![
+ PropertyRequest::DisplayName,
+ PropertyRequest::ResourceType,
+ ])));
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:allprop/>
+ <D:include>
+ <D:displayname/>
+ <D:resourcetype/>
+ </D:include>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propertyupdate() {
+ let orig = PropertyUpdate::<Core>(vec![
+ PropertyUpdateItem::Set(Set(PropValue(vec![Property::GetContentLanguage(
+ "fr-FR".into(),
+ )]))),
+ PropertyUpdateItem::Remove(Remove(PropName(vec![PropertyRequest::DisplayName]))),
+ ]);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propertyupdate xmlns:D="DAV:">
+ <D:set>
+ <D:prop>
+ <D:getcontentlanguage>fr-FR</D:getcontentlanguage>
+ </D:prop>
+ </D:set>
+ <D:remove>
+ <D:prop>
+ <D:displayname/>
+ </D:prop>
+ </D:remove>
+</D:propertyupdate>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(
+ deserialize::<PropertyUpdate::<Core>>(got.as_str()).await,
+ orig
+ )
+ }
+
+ #[tokio::test]
+ async fn rfc_delete_locked2() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![Response {
+ status_or_propstat: StatusOrPropstat::Status(
+ vec![Href("http://www.example.com/container/resource3".into())],
+ Status(http::status::StatusCode::from_u16(423).unwrap()),
+ ),
+ error: Some(Error(vec![Violation::LockTokenSubmitted(vec![])])),
+ responsedescription: None,
+ location: None,
+ }],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>http://www.example.com/container/resource3</D:href>
+ <D:status>HTTP/1.1 423 Locked</D:status>
+ <D:error>
+ <D:lock-token-submitted/>
+ </D:error>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_request() {
+ let orig = LockInfo {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into(),
+ ))),
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:lockinfo xmlns:D="DAV:">
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+</D:lockinfo>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<LockInfo>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_response() {
+ let orig = PropValue::<Core>(vec![Property::LockDiscovery(vec![ActiveLock {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ depth: Depth::Infinity,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into(),
+ ))),
+ timeout: Some(Timeout::Seconds(604800)),
+ locktoken: Some(LockToken(Href(
+ "urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4".into(),
+ ))),
+ lockroot: LockRoot(Href(
+ "http://example.com/workspace/webdav/proposal.doc".into(),
+ )),
+ }])]);
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:prop xmlns:D="DAV:">
+ <D:lockdiscovery>
+ <D:activelock>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:depth>infinity</D:depth>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+ <D:timeout>Second-604800</D:timeout>
+ <D:locktoken>
+ <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href>
+ </D:locktoken>
+ <D:lockroot>
+ <D:href>http://example.com/workspace/webdav/proposal.doc</D:href>
+ </D:lockroot>
+ </D:activelock>
+ </D:lockdiscovery>
+</D:prop>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropValue::<Core>>(got.as_str()).await, orig)
+ }
+}
diff --git a/aero-dav/src/error.rs b/aero-dav/src/error.rs
new file mode 100644
index 0000000..c8f1de1
--- /dev/null
+++ b/aero-dav/src/error.rs
@@ -0,0 +1,62 @@
+use quick_xml::events::attributes::AttrError;
+
+#[derive(Debug)]
+pub enum ParsingError {
+ Recoverable,
+ MissingChild,
+ MissingAttribute,
+ NamespacePrefixAlreadyUsed,
+ WrongToken,
+ TagNotFound,
+ InvalidValue,
+ Utf8Error(std::str::Utf8Error),
+ QuickXml(quick_xml::Error),
+ Chrono(chrono::format::ParseError),
+ Int(std::num::ParseIntError),
+ Eof,
+}
+impl std::fmt::Display for ParsingError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Recoverable => write!(f, "Recoverable"),
+ Self::MissingChild => write!(f, "Missing child"),
+ Self::MissingAttribute => write!(f, "Missing attribute"),
+ Self::NamespacePrefixAlreadyUsed => write!(f, "Namespace prefix already used"),
+ Self::WrongToken => write!(f, "Wrong token"),
+ Self::TagNotFound => write!(f, "Tag not found"),
+ Self::InvalidValue => write!(f, "Invalid value"),
+ Self::Utf8Error(_) => write!(f, "Utf8 Error"),
+ Self::QuickXml(_) => write!(f, "Quick XML error"),
+ Self::Chrono(_) => write!(f, "Chrono error"),
+ Self::Int(_) => write!(f, "Number parsing error"),
+ Self::Eof => write!(f, "Found EOF while expecting data"),
+ }
+ }
+}
+impl std::error::Error for ParsingError {}
+impl From<AttrError> for ParsingError {
+ fn from(value: AttrError) -> Self {
+ Self::QuickXml(value.into())
+ }
+}
+impl From<quick_xml::Error> for ParsingError {
+ fn from(value: quick_xml::Error) -> Self {
+ Self::QuickXml(value)
+ }
+}
+impl From<std::str::Utf8Error> for ParsingError {
+ fn from(value: std::str::Utf8Error) -> Self {
+ Self::Utf8Error(value)
+ }
+}
+impl From<chrono::format::ParseError> for ParsingError {
+ fn from(value: chrono::format::ParseError) -> Self {
+ Self::Chrono(value)
+ }
+}
+
+impl From<std::num::ParseIntError> for ParsingError {
+ fn from(value: std::num::ParseIntError) -> Self {
+ Self::Int(value)
+ }
+}
diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs
new file mode 100644
index 0000000..64be929
--- /dev/null
+++ b/aero-dav/src/lib.rs
@@ -0,0 +1,35 @@
+#![feature(type_alias_impl_trait)]
+#![feature(async_closure)]
+#![feature(trait_alias)]
+
+// utils
+pub mod error;
+pub mod xml;
+
+// webdav
+pub mod decoder;
+pub mod encoder;
+pub mod types;
+
+// calendar
+pub mod caldecoder;
+pub mod calencoder;
+pub mod caltypes;
+
+// acl (partial)
+pub mod acldecoder;
+pub mod aclencoder;
+pub mod acltypes;
+
+// versioning (partial)
+pub mod versioningdecoder;
+pub mod versioningencoder;
+pub mod versioningtypes;
+
+// sync
+pub mod syncdecoder;
+pub mod syncencoder;
+pub mod synctypes;
+
+// final type
+pub mod realization;
diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs
new file mode 100644
index 0000000..76170f8
--- /dev/null
+++ b/aero-dav/src/realization.rs
@@ -0,0 +1,260 @@
+use super::acltypes as acl;
+use super::caltypes as cal;
+use super::error;
+use super::synctypes as sync;
+use super::types as dav;
+use super::versioningtypes as vers;
+use super::xml;
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Disabled(());
+impl xml::QRead<Disabled> for Disabled {
+ async fn qread(_xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ Err(error::ParsingError::Recoverable)
+ }
+}
+impl xml::QWrite for Disabled {
+ async fn qwrite(
+ &self,
+ _xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ unreachable!()
+ }
+}
+
+/// The base WebDAV
+///
+/// Any extension is disabled through an object we can't build
+/// due to a private inner element.
+#[derive(Debug, PartialEq, Clone)]
+pub struct Core {}
+impl dav::Extension for Core {
+ type Error = Disabled;
+ type Property = Disabled;
+ type PropertyRequest = Disabled;
+ type ResourceType = Disabled;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// WebDAV with the base Calendar implementation (RFC4791)
+#[derive(Debug, PartialEq, Clone)]
+pub struct Calendar {}
+impl dav::Extension for Calendar {
+ type Error = cal::Violation;
+ type Property = cal::Property;
+ type PropertyRequest = cal::PropertyRequest;
+ type ResourceType = cal::ResourceType;
+ type ReportType = cal::ReportType<Calendar>;
+ type ReportTypeName = cal::ReportTypeName;
+ type Multistatus = Disabled;
+}
+
+// ACL
+#[derive(Debug, PartialEq, Clone)]
+pub struct Acl {}
+impl dav::Extension for Acl {
+ type Error = Disabled;
+ type Property = acl::Property;
+ type PropertyRequest = acl::PropertyRequest;
+ type ResourceType = acl::ResourceType;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// All merged
+#[derive(Debug, PartialEq, Clone)]
+pub struct All {}
+impl dav::Extension for All {
+ type Error = cal::Violation;
+ type Property = Property<All>;
+ type PropertyRequest = PropertyRequest;
+ type ResourceType = ResourceType;
+ type ReportType = ReportType<All>;
+ type ReportTypeName = ReportTypeName;
+ type Multistatus = Multistatus;
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ Cal(cal::Property),
+ Acl(acl::Property),
+ Sync(sync::Property),
+ Vers(vers::Property<E>),
+}
+impl<E: dav::Extension> xml::QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::<E>::Cal),
+ }
+ match acl::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Acl),
+ }
+ match sync::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Sync),
+ }
+ vers::Property::qread(xml).await.map(Property::Vers)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for Property<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Cal(cal::PropertyRequest),
+ Acl(acl::PropertyRequest),
+ Sync(sync::PropertyRequest),
+ Vers(vers::PropertyRequest),
+}
+impl xml::QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Cal),
+ }
+ match acl::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Acl),
+ }
+ match sync::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Sync),
+ }
+ vers::PropertyRequest::qread(xml)
+ .await
+ .map(PropertyRequest::Vers)
+ }
+}
+impl xml::QWrite for PropertyRequest {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Cal(cal::ResourceType),
+ Acl(acl::ResourceType),
+}
+impl xml::QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ResourceType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ResourceType::Cal),
+ }
+ acl::ResourceType::qread(xml).await.map(ResourceType::Acl)
+ }
+}
+impl xml::QWrite for ResourceType {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Cal(cal::ReportType<E>),
+ Sync(sync::SyncCollection<E>),
+}
+impl<E: dav::Extension> xml::QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(
+ xml: &mut xml::Reader<impl xml::IRead>,
+ ) -> Result<ReportType<E>, error::ParsingError> {
+ match cal::ReportType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportType::Cal),
+ }
+ sync::SyncCollection::qread(xml).await.map(ReportType::Sync)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for ReportType<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Cal(cal::ReportTypeName),
+ Sync(sync::ReportTypeName),
+}
+impl xml::QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ReportTypeName::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportTypeName::Cal),
+ }
+ sync::ReportTypeName::qread(xml)
+ .await
+ .map(ReportTypeName::Sync)
+ }
+}
+impl xml::QWrite for ReportTypeName {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Multistatus {
+ Sync(sync::Multistatus),
+}
+
+impl xml::QWrite for Multistatus {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+impl xml::QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ sync::Multistatus::qread(xml).await.map(Self::Sync)
+ }
+}
diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs
new file mode 100644
index 0000000..2a61dea
--- /dev/null
+++ b/aero-dav/src/syncdecoder.rs
@@ -0,0 +1,248 @@
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::synctypes::*;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncToken);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken)
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-collection").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncCollection);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ SyncToken::qread(xml)
+ .await
+ .map(|sync_token| Multistatus { sync_token })
+ }
+}
+
+impl<E: dav::Extension> QRead<SyncCollection<E>> for SyncCollection<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-collection").await?;
+ let (mut sync_token, mut sync_level, mut limit, mut prop) = (None, None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut sync_token, &mut dirty).await?;
+ xml.maybe_read(&mut sync_level, &mut dirty).await?;
+ xml.maybe_read(&mut limit, &mut dirty).await?;
+ xml.maybe_read(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (sync_token, sync_level, prop) {
+ (Some(sync_token), Some(sync_level), Some(prop)) => Ok(SyncCollection {
+ sync_token,
+ sync_level,
+ limit,
+ prop,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<SyncTokenRequest> for SyncTokenRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = match xml.tag_string().await {
+ Ok(v) => SyncTokenRequest::IncrementalSync(v),
+ Err(ParsingError::Recoverable) => SyncTokenRequest::InitialSync,
+ Err(e) => return Err(e),
+ };
+ xml.close().await?;
+ Ok(token)
+ }
+}
+
+impl QRead<SyncToken> for SyncToken {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(SyncToken(token))
+ }
+}
+
+impl QRead<SyncLevel> for SyncLevel {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-level").await?;
+ let lvl = match xml.tag_string().await?.to_lowercase().as_str() {
+ "1" => SyncLevel::One,
+ "infinite" => SyncLevel::Infinite,
+ _ => return Err(ParsingError::InvalidValue),
+ };
+ xml.close().await?;
+ Ok(lvl)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ {
+ let expected = SyncLevel::One;
+ let src = r#"<D:sync-level xmlns:D="DAV:">1</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected = SyncLevel::Infinite;
+ let src = r#"<D:sync-level xmlns:D="DAV:">infinite</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ {
+ let expected = SyncTokenRequest::InitialSync;
+ let src = r#"<D:sync-token xmlns:D="DAV:"/>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected =
+ SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into());
+ let src =
+ r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ let expected = SyncToken("http://example.com/ns/sync/1232".into());
+ let src = r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncToken>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token>http://example.com/ns/sync/1232</D:sync-token>
+ <D:sync-level>1</D:sync-level>
+ <D:limit>
+ <D:nresults>100</D:nresults>
+ </D:limit>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token/>
+ <D:sync-level>infinite</D:sync-level>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
+ realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
+ )]);
+ let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
+ let got = deserialize::<dav::PropName<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ let expected = dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]);
+ let src = r#"<prop xmlns="DAV:">
+ <sync-token>http://example.com/ns/sync/1232</sync-token>
+ <supported-report-set>
+ <supported-report>
+ <report><sync-collection/></report>
+ </supported-report>
+ </supported-report-set>
+ </prop>"#;
+ let got = deserialize::<dav::PropValue<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs
new file mode 100644
index 0000000..55f7ad6
--- /dev/null
+++ b/aero-dav/src/syncencoder.rs
@@ -0,0 +1,227 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::synctypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken(token) => token.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken => {
+ let start = xml.create_dav_element("sync-token");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncCollection => {
+ let start = xml.create_dav_element("sync-collection");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for Multistatus {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ self.sync_token.qwrite(xml).await
+ }
+}
+
+impl<E: Extension> QWrite for SyncCollection<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-collection");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.sync_token.qwrite(xml).await?;
+ self.sync_level.qwrite(xml).await?;
+ if let Some(limit) = &self.limit {
+ limit.qwrite(xml).await?;
+ }
+ self.prop.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for SyncTokenRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+
+ match self {
+ Self::InitialSync => xml.q.write_event_async(Event::Empty(start)).await,
+ Self::IncrementalSync(uri) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(uri.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for SyncToken {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.0.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for SyncLevel {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-level");
+ let end = start.to_end();
+ let text = match self {
+ Self::One => "1",
+ Self::Infinite => "infinite",
+ };
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(text)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ src.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ let res = rdr.find().await.unwrap();
+
+ // check
+ assert_eq!(src, &res);
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ serialize_deserialize(&SyncLevel::One).await;
+ serialize_deserialize(&SyncLevel::Infinite).await;
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ serialize_deserialize(&SyncTokenRequest::InitialSync).await;
+ serialize_deserialize(&SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ serialize_deserialize(&SyncToken("http://example.com/ns/sync/1232".into())).await;
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ serialize_deserialize(&dav::PropName::<All>(vec![
+ dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
+ PropertyRequest::SyncToken,
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ serialize_deserialize(&dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn multistatus_ext() {
+ serialize_deserialize(&dav::Multistatus::<All> {
+ responses: vec![dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::Status(
+ vec![dav::Href("/".into())],
+ dav::Status(http::status::StatusCode::OK),
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ }],
+ responsedescription: None,
+ extension: Some(realization::Multistatus::Sync(Multistatus {
+ sync_token: SyncToken("http://example.com/ns/sync/1232".into()),
+ })),
+ })
+ .await;
+ }
+}
diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs
new file mode 100644
index 0000000..2a14221
--- /dev/null
+++ b/aero-dav/src/synctypes.rs
@@ -0,0 +1,86 @@
+use super::types as dav;
+use super::versioningtypes as vers;
+
+// RFC 6578
+// https://datatracker.ietf.org/doc/html/rfc6578
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SyncToken,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ SyncToken(SyncToken),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ SyncCollection,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus {
+ pub sync_token: SyncToken,
+}
+
+//@FIXME add SyncToken to Multistatus
+
+/// Name: sync-collection
+///
+/// Namespace: DAV:
+///
+/// Purpose: WebDAV report used to synchronize data between client and
+/// server.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
+///
+/// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
+/// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncCollection<E: dav::Extension> {
+ pub sync_token: SyncTokenRequest,
+ pub sync_level: SyncLevel,
+ pub limit: Option<vers::Limit>,
+ pub prop: dav::PropName<E>,
+}
+
+/// Name: sync-token
+///
+/// Namespace: DAV:
+///
+/// Purpose: The synchronization token provided by the server and
+/// returned by the client.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-token CDATA>
+///
+/// <!-- Text MUST be a URI -->
+/// Used by multistatus
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncToken(pub String);
+
+/// Used by propfind and report sync-collection
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncTokenRequest {
+ InitialSync,
+ IncrementalSync(String),
+}
+
+/// Name: sync-level
+///
+/// Namespace: DAV:
+///
+/// Purpose: Indicates the "scope" of the synchronization report
+/// request.
+///
+/// Description: See Section 3.3.
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncLevel {
+ One,
+ Infinite,
+}
diff --git a/aero-dav/src/types.rs b/aero-dav/src/types.rs
new file mode 100644
index 0000000..61a6fe9
--- /dev/null
+++ b/aero-dav/src/types.rs
@@ -0,0 +1,964 @@
+#![allow(dead_code)]
+use std::fmt::Debug;
+
+use super::xml;
+use chrono::{DateTime, FixedOffset};
+
+/// It's how we implement a DAV extension
+/// (That's the dark magic part...)
+pub trait Extension: std::fmt::Debug + PartialEq + Clone {
+ type Error: xml::Node<Self::Error>;
+ type Property: xml::Node<Self::Property>;
+ type PropertyRequest: xml::Node<Self::PropertyRequest>;
+ type ResourceType: xml::Node<Self::ResourceType>;
+ type ReportType: xml::Node<Self::ReportType>;
+ type ReportTypeName: xml::Node<Self::ReportTypeName>;
+ type Multistatus: xml::Node<Self::Multistatus>;
+}
+
+/// 14.1. activelock XML Element
+///
+/// Name: activelock
+///
+/// Purpose: Describes a lock on a resource.
+/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
+/// locktoken?, lockroot)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct ActiveLock {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+ pub depth: Depth,
+ pub owner: Option<Owner>,
+ pub timeout: Option<Timeout>,
+ pub locktoken: Option<LockToken>,
+ pub lockroot: LockRoot,
+}
+
+/// 14.3 collection XML Element
+///
+/// Name: collection
+///
+/// Purpose: Identifies the associated resource as a collection. The
+/// DAV:resourcetype property of a collection resource MUST contain
+/// this element. It is normally empty but extensions may add sub-
+/// elements.
+///
+/// <!ELEMENT collection EMPTY >
+#[derive(Debug, PartialEq)]
+pub struct Collection {}
+
+/// 14.4 depth XML Element
+///
+/// Name: depth
+///
+/// Purpose: Used for representing depth values in XML content (e.g.,
+/// in lock information).
+///
+/// Value: "0" | "1" | "infinity"
+///
+/// <!ELEMENT depth (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum Depth {
+ Zero,
+ One,
+ Infinity,
+}
+
+/// 14.5 error XML Element
+///
+/// Name: error
+///
+/// Purpose: Error responses, particularly 403 Forbidden and 409
+/// Conflict, sometimes need more information to indicate what went
+/// wrong. In these cases, servers MAY return an XML response body
+/// with a document element of 'error', containing child elements
+/// identifying particular condition codes.
+///
+/// Description: Contains at least one XML element, and MUST NOT
+/// contain text or mixed content. Any element that is a child of the
+/// 'error' element is considered to be a precondition or
+/// postcondition code. Unrecognized elements MUST be ignored.
+///
+/// <!ELEMENT error ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Error<E: Extension>(pub Vec<Violation<E>>);
+#[derive(Debug, PartialEq, Clone)]
+pub enum Violation<E: Extension> {
+ /// Name: lock-token-matches-request-uri
+ ///
+ /// Use with: 409 Conflict
+ ///
+ /// Purpose: (precondition) -- A request may include a Lock-Token header
+ /// to identify a lock for the UNLOCK method. However, if the
+ /// Request-URI does not fall within the scope of the lock identified
+ /// by the token, the server SHOULD use this error. The lock may have
+ /// a scope that does not include the Request-URI, or the lock could
+ /// have disappeared, or the token may be invalid.
+ LockTokenMatchesRequestUri,
+
+ /// Name: lock-token-submitted (precondition)
+ ///
+ /// Use with: 423 Locked
+ ///
+ /// Purpose: The request could not succeed because a lock token should
+ /// have been submitted. This element, if present, MUST contain at
+ /// least one URL of a locked resource that prevented the request. In
+ /// cases of MOVE, COPY, and DELETE where collection locks are
+ /// involved, it can be difficult for the client to find out which
+ /// locked resource made the request fail -- but the server is only
+ /// responsible for returning one such locked resource. The server
+ /// MAY return every locked resource that prevented the request from
+ /// succeeding if it knows them all.
+ ///
+ /// <!ELEMENT lock-token-submitted (href+) >
+ LockTokenSubmitted(Vec<Href>),
+
+ /// Name: no-conflicting-lock (precondition)
+ ///
+ /// Use with: Typically 423 Locked
+ ///
+ /// Purpose: A LOCK request failed due the presence of an already
+ /// existing conflicting lock. Note that a lock can be in conflict
+ /// although the resource to which the request was directed is only
+ /// indirectly locked. In this case, the precondition code can be
+ /// used to inform the client about the resource that is the root of
+ /// the conflicting lock, avoiding a separate lookup of the
+ /// "lockdiscovery" property.
+ ///
+ /// <!ELEMENT no-conflicting-lock (href)* >
+ NoConflictingLock(Vec<Href>),
+
+ /// Name: no-external-entities
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- If the server rejects a client request
+ /// because the request body contains an external entity, the server
+ /// SHOULD use this error.
+ NoExternalEntities,
+
+ /// Name: preserved-live-properties
+ ///
+ /// Use with: 409 Conflict
+ ///
+ /// Purpose: (postcondition) -- The server received an otherwise-valid
+ /// MOVE or COPY request, but cannot maintain the live properties with
+ /// the same behavior at the destination. It may be that the server
+ /// only supports some live properties in some parts of the
+ /// repository, or simply has an internal error.
+ PreservedLiveProperties,
+
+ /// Name: propfind-finite-depth
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- This server does not allow infinite-depth
+ /// PROPFIND requests on collections.
+ PropfindFiniteDepth,
+
+ /// Name: cannot-modify-protected-property
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- The client attempted to set a protected
+ /// property in a PROPPATCH (such as DAV:getetag). See also
+ /// [RFC3253], Section 3.12.
+ CannotModifyProtectedProperty,
+
+ /// Specific errors
+ Extension(E::Error),
+}
+
+/// 14.6. exclusive XML Element
+///
+/// Name: exclusive
+///
+/// Purpose: Specifies an exclusive lock.
+///
+/// <!ELEMENT exclusive EMPTY >
+#[derive(Debug, PartialEq)]
+pub struct Exclusive {}
+
+/// 14.7. href XML Element
+///
+/// Name: href
+///
+/// Purpose: MUST contain a URI or a relative reference.
+///
+/// Description: There may be limits on the value of 'href' depending
+/// on the context of its use. Refer to the specification text where
+/// 'href' is used to see what limitations apply in each case.
+///
+/// Value: Simple-ref
+///
+/// <!ELEMENT href (#PCDATA)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Href(pub String);
+
+/// 14.8. include XML Element
+///
+/// Name: include
+///
+/// Purpose: Any child element represents the name of a property to be
+/// included in the PROPFIND response. All elements inside an
+/// 'include' XML element MUST define properties related to the
+/// resource, although possible property names are in no way limited
+/// to those property names defined in this document or other
+/// standards. This element MUST NOT contain text or mixed content.
+///
+/// <!ELEMENT include ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>);
+
+/// 14.9. location XML Element
+///
+/// Name: location
+///
+/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section
+/// 14.30) for use with some status codes (such as 201 and the 300
+/// series codes). When these codes are used inside a 'multistatus'
+/// element, the 'location' element can be used to provide the
+/// accompanying Location header value.
+///
+/// Description: Contains a single href element with the same value
+/// that would be used in a Location header.
+///
+/// <!ELEMENT location (href)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Location(pub Href);
+
+/// 14.10. lockentry XML Element
+///
+/// Name: lockentry
+///
+/// Purpose: Defines the types of locks that can be used with the
+/// resource.
+///
+/// <!ELEMENT lockentry (lockscope, locktype) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockEntry {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+}
+
+/// 14.11. lockinfo XML Element
+///
+/// Name: lockinfo
+///
+/// Purpose: The 'lockinfo' XML element is used with a LOCK method to
+/// specify the type of lock the client wishes to have created.
+///
+/// <!ELEMENT lockinfo (lockscope, locktype, owner?) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockInfo {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+ pub owner: Option<Owner>,
+}
+
+/// 14.12. lockroot XML Element
+///
+/// Name: lockroot
+///
+/// Purpose: Contains the root URL of the lock, which is the URL
+/// through which the resource was addressed in the LOCK request.
+///
+/// Description: The href element contains the root of the lock. The
+/// server SHOULD include this in all DAV:lockdiscovery property
+/// values and the response to LOCK requests.
+///
+/// <!ELEMENT lockroot (href) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockRoot(pub Href);
+
+/// 14.13. lockscope XML Element
+///
+/// Name: lockscope
+///
+/// Purpose: Specifies whether a lock is an exclusive lock, or a shared
+/// lock.
+/// <!ELEMENT lockscope (exclusive | shared) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum LockScope {
+ Exclusive,
+ Shared,
+}
+
+/// 14.14. locktoken XML Element
+///
+/// Name: locktoken
+///
+/// Purpose: The lock token associated with a lock.
+///
+/// Description: The href contains a single lock token URI, which
+/// refers to the lock.
+///
+/// <!ELEMENT locktoken (href) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockToken(pub Href);
+
+/// 14.15. locktype XML Element
+///
+/// Name: locktype
+///
+/// Purpose: Specifies the access type of a lock. At present, this
+/// specification only defines one lock type, the write lock.
+///
+/// <!ELEMENT locktype (write) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum LockType {
+ /// 14.30. write XML Element
+ ///
+ /// Name: write
+ ///
+ /// Purpose: Specifies a write lock.
+ ///
+ ///
+ /// <!ELEMENT write EMPTY >
+ Write,
+}
+
+/// 14.16. multistatus XML Element
+///
+/// Name: multistatus
+///
+/// Purpose: Contains multiple response messages.
+///
+/// Description: The 'responsedescription' element at the top level is
+/// used to provide a general message describing the overarching
+/// nature of the response. If this value is available, an
+/// application may use it instead of presenting the individual
+/// response descriptions contained within the responses.
+///
+/// <!ELEMENT multistatus (response*, responsedescription?) >
+///
+/// In WebDAV sync (rfc6578), multistatus is extended:
+///
+/// <!ELEMENT multistatus (response*, responsedescription?, sync-token?) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus<E: Extension> {
+ pub responses: Vec<Response<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+ pub extension: Option<E::Multistatus>,
+}
+
+/// 14.17. owner XML Element
+///
+/// Name: owner
+///
+/// Purpose: Holds client-supplied information about the creator of a
+/// lock.
+///
+/// Description: Allows a client to provide information sufficient for
+/// either directly contacting a principal (such as a telephone number
+/// or Email URI), or for discovering the principal (such as the URL
+/// of a homepage) who created a lock. The value provided MUST be
+/// treated as a dead property in terms of XML Information Item
+/// preservation. The server MUST NOT alter the value unless the
+/// owner value provided by the client is empty. For a certain amount
+/// of interoperability between different client implementations, if
+/// clients have URI-formatted contact information for the lock
+/// creator suitable for user display, then clients SHOULD put those
+/// URIs in 'href' child elements of the 'owner' element.
+///
+/// Extensibility: MAY be extended with child elements, mixed content,
+/// text content or attributes.
+///
+/// <!ELEMENT owner ANY >
+//@FIXME might need support for an extension
+#[derive(Debug, PartialEq, Clone)]
+pub enum Owner {
+ Txt(String),
+ Href(Href),
+ Unknown,
+}
+
+/// 14.18. prop XML Element
+///
+/// Name: prop
+///
+/// Purpose: Contains properties related to a resource.
+///
+/// Description: A generic container for properties defined on
+/// resources. All elements inside a 'prop' XML element MUST define
+/// properties related to the resource, although possible property
+/// names are in no way limited to those property names defined in
+/// this document or other standards. This element MUST NOT contain
+/// text or mixed content.
+///
+/// <!ELEMENT prop ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropValue<E: Extension>(pub Vec<Property<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct AnyProp<E: Extension>(pub Vec<AnyProperty<E>>);
+
+/// 14.19. propertyupdate XML Element
+///
+/// Name: propertyupdate
+///
+/// Purpose: Contains a request to alter the properties on a resource.
+///
+/// Description: This XML element is a container for the information
+/// required to modify the properties on the resource.
+///
+/// <!ELEMENT propertyupdate (remove | set)+ >
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyUpdateItem<E: Extension> {
+ Remove(Remove<E>),
+ Set(Set<E>),
+}
+
+/// 14.2 allprop XML Element
+///
+/// Name: allprop
+///
+/// Purpose: Specifies that all names and values of dead properties and
+/// the live properties defined by this document existing on the
+/// resource are to be returned.
+///
+/// <!ELEMENT allprop EMPTY >
+///
+/// ---
+///
+/// 14.21. propname XML Element
+///
+/// Name: propname
+///
+/// Purpose: Specifies that only a list of property names on the
+/// resource is to be returned.
+///
+/// <!ELEMENT propname EMPTY >
+///
+/// ---
+///
+/// 14.20. propfind XML Element
+///
+/// Name: propfind
+///
+/// Purpose: Specifies the properties to be returned from a PROPFIND
+/// method. Four special elements are specified for use with
+/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If
+/// 'prop' is used inside 'propfind', it MUST NOT contain property
+/// values.
+///
+/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropFind<E: Extension> {
+ PropName,
+ AllProp(Option<Include<E>>),
+ Prop(PropName<E>),
+}
+
+/// 14.22 propstat XML Element
+///
+/// Name: propstat
+///
+/// Purpose: Groups together a prop and status element that is
+/// associated with a particular 'href' element.
+///
+/// Description: The propstat XML element MUST contain one prop XML
+/// element and one status XML element. The contents of the prop XML
+/// element MUST only list the names of properties to which the result
+/// in the status element applies. The optional precondition/
+/// postcondition element and 'responsedescription' text also apply to
+/// the properties named in 'prop'.
+///
+/// <!ELEMENT propstat (prop, status, error?, responsedescription?) >
+///
+/// ---
+///
+///
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropStat<E: Extension> {
+ pub prop: AnyProp<E>,
+ pub status: Status,
+ pub error: Option<Error<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+}
+
+/// 14.23. remove XML Element
+///
+/// Name: remove
+///
+/// Purpose: Lists the properties to be removed from a resource.
+///
+/// Description: Remove instructs that the properties specified in prop
+/// should be removed. Specifying the removal of a property that does
+/// not exist is not an error. All the XML elements in a 'prop' XML
+/// element inside of a 'remove' XML element MUST be empty, as only
+/// the names of properties to be removed are required.
+///
+/// <!ELEMENT remove (prop) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Remove<E: Extension>(pub PropName<E>);
+
+/// 14.24. response XML Element
+///
+/// Name: response
+///
+/// Purpose: Holds a single response describing the effect of a method
+/// on resource and/or its properties.
+///
+/// Description: The 'href' element contains an HTTP URL pointing to a
+/// WebDAV resource when used in the 'response' container. A
+/// particular 'href' value MUST NOT appear more than once as the
+/// child of a 'response' XML element under a 'multistatus' XML
+/// element. This requirement is necessary in order to keep
+/// processing costs for a response to linear time. Essentially, this
+/// prevents having to search in order to group together all the
+/// responses by 'href'. There are, however, no requirements
+/// regarding ordering based on 'href' values. The optional
+/// precondition/postcondition element and 'responsedescription' text
+/// can provide additional information about this resource relative to
+/// the request or result.
+///
+/// <!ELEMENT response (href, ((href*, status)|(propstat+)),
+/// error?, responsedescription? , location?) >
+///
+/// --- rewritten as ---
+/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?>
+#[derive(Debug, PartialEq, Clone)]
+pub enum StatusOrPropstat<E: Extension> {
+ // One status, multiple hrefs...
+ Status(Vec<Href>, Status),
+ // A single href, multiple properties...
+ PropStat(Href, Vec<PropStat<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Response<E: Extension> {
+ pub status_or_propstat: StatusOrPropstat<E>,
+ pub error: Option<Error<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+ pub location: Option<Location>,
+}
+
+/// 14.25. responsedescription XML Element
+///
+/// Name: responsedescription
+///
+/// Purpose: Contains information about a status response within a
+/// Multi-Status.
+///
+/// Description: Provides information suitable to be presented to a
+/// user.
+///
+/// <!ELEMENT responsedescription (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct ResponseDescription(pub String);
+
+/// 14.26. set XML Element
+///
+/// Name: set
+///
+/// Purpose: Lists the property values to be set for a resource.
+///
+/// Description: The 'set' element MUST contain only a 'prop' element.
+/// The elements contained by the 'prop' element inside the 'set'
+/// element MUST specify the name and value of properties that are set
+/// on the resource identified by Request-URI. If a property already
+/// exists, then its value is replaced. Language tagging information
+/// appearing in the scope of the 'prop' element (in the "xml:lang"
+/// attribute, if present) MUST be persistently stored along with the
+/// property, and MUST be subsequently retrievable using PROPFIND.
+///
+/// <!ELEMENT set (prop) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Set<E: Extension>(pub PropValue<E>);
+
+/// 14.27. shared XML Element
+///
+/// Name: shared
+///
+/// Purpose: Specifies a shared lock.
+///
+///
+/// <!ELEMENT shared EMPTY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Shared {}
+
+/// 14.28. status XML Element
+///
+/// Name: status
+///
+/// Purpose: Holds a single HTTP status-line.
+///
+/// Value: status-line (defined in Section 6.1 of [RFC2616])
+///
+/// <!ELEMENT status (#PCDATA) >
+//@FIXME: Better typing is possible with an enum for example
+#[derive(Debug, PartialEq, Clone)]
+pub struct Status(pub http::status::StatusCode);
+
+/// 14.29. timeout XML Element
+///
+/// Name: timeout
+///
+/// Purpose: The number of seconds remaining before a lock expires.
+///
+/// Value: TimeType (defined in Section 10.7)
+///
+///
+/// <!ELEMENT timeout (#PCDATA) >
+///
+/// TimeOut = "Timeout" ":" 1#TimeType
+/// TimeType = ("Second-" DAVTimeOutVal | "Infinite")
+/// ; No LWS allowed within TimeType
+/// DAVTimeOutVal = 1*DIGIT
+///
+/// Clients MAY include Timeout request headers in their LOCK requests.
+/// However, the server is not required to honor or even consider these
+/// requests. Clients MUST NOT submit a Timeout request header with any
+/// method other than a LOCK method.
+///
+/// The "Second" TimeType specifies the number of seconds that will
+/// elapse between granting of the lock at the server, and the automatic
+/// removal of the lock. The timeout value for TimeType "Second" MUST
+/// NOT be greater than 2^32-1.
+#[derive(Debug, PartialEq, Clone)]
+pub enum Timeout {
+ Seconds(u32),
+ Infinite,
+}
+
+/// 15. DAV Properties
+///
+/// For DAV properties, the name of the property is also the same as the
+/// name of the XML element that contains its value. In the section
+/// below, the final line of each section gives the element type
+/// declaration using the format defined in [REC-XML]. The "Value"
+/// field, where present, specifies further restrictions on the allowable
+/// contents of the XML element using BNF (i.e., to further restrict the
+/// values of a PCDATA element).
+///
+/// A protected property is one that cannot be changed with a PROPPATCH
+/// request. There may be other requests that would result in a change
+/// to a protected property (as when a LOCK request affects the value of
+/// DAV:lockdiscovery). Note that a given property could be protected on
+/// one type of resource, but not protected on another type of resource.
+///
+/// A computed property is one with a value defined in terms of a
+/// computation (based on the content and other properties of that
+/// resource, or even of some other resource). A computed property is
+/// always a protected property.
+///
+/// COPY and MOVE behavior refers to local COPY and MOVE operations.
+///
+/// For properties defined based on HTTP GET response headers (DAV:get*),
+/// the header value could include LWS as defined in [RFC2616], Section
+/// 4.2. Server implementors SHOULD strip LWS from these values before
+/// using as WebDAV property values.
+#[derive(Debug, PartialEq, Clone)]
+pub enum AnyProperty<E: Extension> {
+ Request(PropertyRequest<E>),
+ Value(Property<E>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest<E: Extension> {
+ CreationDate,
+ DisplayName,
+ GetContentLanguage,
+ GetContentLength,
+ GetContentType,
+ GetEtag,
+ GetLastModified,
+ LockDiscovery,
+ ResourceType,
+ SupportedLock,
+ Extension(E::PropertyRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: Extension> {
+ /// 15.1. creationdate Property
+ ///
+ /// Name: creationdate
+ ///
+ /// Purpose: Records the time and date the resource was created.
+ ///
+ /// Value: date-time (defined in [RFC3339], see the ABNF in Section
+ /// 5.6.)
+ ///
+ /// Protected: MAY be protected. Some servers allow DAV:creationdate
+ /// to be changed to reflect the time the document was created if that
+ /// is more meaningful to the user (rather than the time it was
+ /// uploaded). Thus, clients SHOULD NOT use this property in
+ /// synchronization logic (use DAV:getetag instead).
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be kept during a
+ /// MOVE operation, but is normally re-initialized when a resource is
+ /// created with a COPY. It should not be set in a COPY.
+ ///
+ /// Description: The DAV:creationdate property SHOULD be defined on all
+ /// DAV compliant resources. If present, it contains a timestamp of
+ /// the moment when the resource was created. Servers that are
+ /// incapable of persistently recording the creation date SHOULD
+ /// instead leave it undefined (i.e. report "Not Found").
+ ///
+ /// <!ELEMENT creationdate (#PCDATA) >
+ CreationDate(DateTime<FixedOffset>),
+
+ /// 15.2. displayname Property
+ ///
+ /// Name: displayname
+ ///
+ /// Purpose: Provides a name for the resource that is suitable for
+ /// presentation to a user.
+ ///
+ /// Value: Any text.
+ ///
+ /// Protected: SHOULD NOT be protected. Note that servers implementing
+ /// [RFC2518] might have made this a protected property as this is a
+ /// new requirement.
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: Contains a description of the resource that is
+ /// suitable for presentation to a user. This property is defined on
+ /// the resource, and hence SHOULD have the same value independent of
+ /// the Request-URI used to retrieve it (thus, computing this property
+ /// based on the Request-URI is deprecated). While generic clients
+ /// might display the property value to end users, client UI designers
+ /// must understand that the method for identifying resources is still
+ /// the URL. Changes to DAV:displayname do not issue moves or copies
+ /// to the server, but simply change a piece of meta-data on the
+ /// individual resource. Two resources can have the same DAV:
+ /// displayname value even within the same collection.
+ ///
+ /// <!ELEMENT displayname (#PCDATA) >
+ DisplayName(String),
+
+ /// 15.3. getcontentlanguage Property
+ ///
+ /// Name: getcontentlanguage
+ ///
+ /// Purpose: Contains the Content-Language header value (from Section
+ /// 14.12 of [RFC2616]) as it would be returned by a GET without
+ /// accept headers.
+ ///
+ /// Value: language-tag (language-tag is defined in Section 3.10 of
+ /// [RFC2616])
+ ///
+ /// Protected: SHOULD NOT be protected, so that clients can reset the
+ /// language. Note that servers implementing [RFC2518] might have
+ /// made this a protected property as this is a new requirement.
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: The DAV:getcontentlanguage property MUST be defined on
+ /// any DAV-compliant resource that returns the Content-Language
+ /// header on a GET.
+ ///
+ /// <!ELEMENT getcontentlanguage (#PCDATA) >
+ GetContentLanguage(String),
+
+ /// 15.4. getcontentlength Property
+ ///
+ /// Name: getcontentlength
+ ///
+ /// Purpose: Contains the Content-Length header returned by a GET
+ /// without accept headers.
+ ///
+ /// Value: See Section 14.13 of [RFC2616].
+ ///
+ /// Protected: This property is computed, therefore protected.
+ ///
+ /// Description: The DAV:getcontentlength property MUST be defined on
+ /// any DAV-compliant resource that returns the Content-Length header
+ /// in response to a GET.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the size of
+ /// the destination resource, not the value of the property on the
+ /// source resource.
+ ///
+ /// <!ELEMENT getcontentlength (#PCDATA) >
+ GetContentLength(u64),
+
+ /// 15.5. getcontenttype Property
+ ///
+ /// Name: getcontenttype
+ ///
+ /// Purpose: Contains the Content-Type header value (from Section 14.17
+ /// of [RFC2616]) as it would be returned by a GET without accept
+ /// headers.
+ ///
+ /// Value: media-type (defined in Section 3.7 of [RFC2616])
+ ///
+ /// Protected: Potentially protected if the server prefers to assign
+ /// content types on its own (see also discussion in Section 9.7.1).
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: This property MUST be defined on any DAV-compliant
+ /// resource that returns the Content-Type header in response to a
+ /// GET.
+ ///
+ /// <!ELEMENT getcontenttype (#PCDATA) >
+ GetContentType(String),
+
+ /// 15.6. getetag Property
+ ///
+ /// Name: getetag
+ ///
+ /// Purpose: Contains the ETag header value (from Section 14.19 of
+ /// [RFC2616]) as it would be returned by a GET without accept
+ /// headers.
+ ///
+ /// Value: entity-tag (defined in Section 3.11 of [RFC2616])
+ ///
+ /// Protected: MUST be protected because this value is created and
+ /// controlled by the server.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the final
+ /// state of the destination resource, not the value of the property
+ /// on the source resource. Also note the considerations in
+ /// Section 8.8.
+ ///
+ /// Description: The getetag property MUST be defined on any DAV-
+ /// compliant resource that returns the Etag header. Refer to Section
+ /// 3.11 of RFC 2616 for a complete definition of the semantics of an
+ /// ETag, and to Section 8.6 for a discussion of ETags in WebDAV.
+ ///
+ /// <!ELEMENT getetag (#PCDATA) >
+ GetEtag(String),
+
+ /// 15.7. getlastmodified Property
+ ///
+ /// Name: getlastmodified
+ ///
+ /// Purpose: Contains the Last-Modified header value (from Section
+ /// 14.29 of [RFC2616]) as it would be returned by a GET method
+ /// without accept headers.
+ ///
+ /// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616])
+ ///
+ /// Protected: SHOULD be protected because some clients may rely on the
+ /// value for appropriate caching behavior, or on the value of the
+ /// Last-Modified header to which this property is linked.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the last
+ /// modified date of the destination resource, not the value of the
+ /// property on the source resource. Note that some server
+ /// implementations use the file system date modified value for the
+ /// DAV:getlastmodified value, and this can be preserved in a MOVE
+ /// even when the HTTP Last-Modified value SHOULD change. Note that
+ /// since [RFC2616] requires clients to use ETags where provided, a
+ /// server implementing ETags can count on clients using a much better
+ /// mechanism than modification dates for offline synchronization or
+ /// cache control. Also note the considerations in Section 8.8.
+ ///
+ /// Description: The last-modified date on a resource SHOULD only
+ /// reflect changes in the body (the GET responses) of the resource.
+ /// A change in a property only SHOULD NOT cause the last-modified
+ /// date to change, because clients MAY rely on the last-modified date
+ /// to know when to overwrite the existing body. The DAV:
+ /// getlastmodified property MUST be defined on any DAV-compliant
+ /// resource that returns the Last-Modified header in response to a
+ /// GET.
+ ///
+ /// <!ELEMENT getlastmodified (#PCDATA) >
+ GetLastModified(DateTime<FixedOffset>),
+
+ /// 15.8. lockdiscovery Property
+ ///
+ /// Name: lockdiscovery
+ ///
+ /// Purpose: Describes the active locks on a resource
+ ///
+ /// Protected: MUST be protected. Clients change the list of locks
+ /// through LOCK and UNLOCK, not through PROPPATCH.
+ ///
+ /// COPY/MOVE behavior: The value of this property depends on the lock
+ /// state of the destination, not on the locks of the source resource.
+ /// Recall that locks are not moved in a MOVE operation.
+ ///
+ /// Description: Returns a listing of who has a lock, what type of lock
+ /// he has, the timeout type and the time remaining on the timeout,
+ /// and the associated lock token. Owner information MAY be omitted
+ /// if it is considered sensitive. If there are no locks, but the
+ /// server supports locks, the property will be present but contain
+ /// zero 'activelock' elements. If there are one or more locks, an
+ /// 'activelock' element appears for each lock on the resource. This
+ /// property is NOT lockable with respect to write locks (Section 7).
+ ///
+ /// <!ELEMENT lockdiscovery (activelock)* >
+ LockDiscovery(Vec<ActiveLock>),
+
+ /// 15.9. resourcetype Property
+ ///
+ /// Name: resourcetype
+ ///
+ /// Purpose: Specifies the nature of the resource.
+ ///
+ /// Protected: SHOULD be protected. Resource type is generally decided
+ /// through the operation creating the resource (MKCOL vs PUT), not by
+ /// PROPPATCH.
+ ///
+ /// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in
+ /// the same type of resource at the destination.
+ ///
+ /// Description: MUST be defined on all DAV-compliant resources. Each
+ /// child element identifies a specific type the resource belongs to,
+ /// such as 'collection', which is the only resource type defined by
+ /// this specification (see Section 14.3). If the element contains
+ /// the 'collection' child element plus additional unrecognized
+ /// elements, it should generally be treated as a collection. If the
+ /// element contains no recognized child elements, it should be
+ /// treated as a non-collection resource. The default value is empty.
+ /// This element MUST NOT contain text or mixed content. Any custom
+ /// child element is considered to be an identifier for a resource
+ /// type.
+ ///
+ /// Example: (fictional example to show extensibility)
+ ///
+ /// <x:resourcetype xmlns:x="DAV:">
+ /// <x:collection/>
+ /// <f:search-results xmlns:f="http://www.example.com/ns"/>
+ /// </x:resourcetype>
+ ResourceType(Vec<ResourceType<E>>),
+
+ /// 15.10. supportedlock Property
+ ///
+ /// Name: supportedlock
+ ///
+ /// Purpose: To provide a listing of the lock capabilities supported by
+ /// the resource.
+ ///
+ /// Protected: MUST be protected. Servers, not clients, determine what
+ /// lock mechanisms are supported.
+ /// COPY/MOVE behavior: This property value is dependent on the kind of
+ /// locks supported at the destination, not on the value of the
+ /// property at the source resource. Servers attempting to COPY to a
+ /// destination should not attempt to set this property at the
+ /// destination.
+ ///
+ /// Description: Returns a listing of the combinations of scope and
+ /// access types that may be specified in a lock request on the
+ /// resource. Note that the actual contents are themselves controlled
+ /// by access controls, so a server is not required to provide
+ /// information the client is not authorized to see. This property is
+ /// NOT lockable with respect to write locks (Section 7).
+ ///
+ /// <!ELEMENT supportedlock (lockentry)* >
+ SupportedLock(Vec<LockEntry>),
+
+ /// Any extension
+ Extension(E::Property),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType<E: Extension> {
+ Collection,
+ Extension(E::ResourceType),
+}
diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs
new file mode 100644
index 0000000..a0a3ddf
--- /dev/null
+++ b/aero-dav/src/versioningdecoder.rs
@@ -0,0 +1,132 @@
+use super::error::ParsingError;
+use super::types as dav;
+use super::versioningtypes::*;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+// -- extensions ---
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedReportSet);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ let supported_reports = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedReportSet(supported_reports));
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl<E: dav::Extension> QRead<SupportedReport<E>> for SupportedReport<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "supported-report").await?;
+ let r = xml.find().await?;
+ xml.close().await?;
+ Ok(SupportedReport(r))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportName<E>> for ReportName<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "report").await?;
+
+ let final_result = if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::ExpandProperty)
+ } else {
+ let x = match xml.maybe_find().await? {
+ Some(v) => v,
+ None => return Err(ParsingError::MissingChild),
+ };
+ Ok(ReportName::Extension(x))
+ //E::ReportTypeName::qread(xml).await.map(ReportName::Extension)
+ };
+
+ xml.close().await?;
+ final_result
+ }
+}
+
+impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("version-tree is not implemented, skipping");
+ Ok(Report::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("expand-property is not implemented, skipping");
+ Ok(Report::ExpandProperty)
+ } else {
+ E::ReportType::qread(xml).await.map(Report::Extension)
+ }
+ }
+}
+
+impl QRead<Limit> for Limit {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "limit").await?;
+ let nres = xml.find().await?;
+ xml.close().await?;
+ Ok(Limit(nres))
+ }
+}
+
+impl QRead<NResults> for NResults {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "nresults").await?;
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ Ok(NResults(sz))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let expected = NResults(100);
+ let src = r#"<D:nresults xmlns:D="DAV:">100</D:nresults>"#;
+ let got = deserialize::<NResults>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let expected = Limit(NResults(1024));
+ let src = r#"<D:limit xmlns:D="DAV:">
+ <D:nresults>1024</D:nresults>
+ </D:limit>"#;
+ let got = deserialize::<Limit>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/versioningencoder.rs b/aero-dav/src/versioningencoder.rs
new file mode 100644
index 0000000..c061f07
--- /dev/null
+++ b/aero-dav/src/versioningencoder.rs
@@ -0,0 +1,143 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::types::Extension;
+use super::versioningtypes::*;
+use super::xml::{IWrite, QWrite, Writer};
+
+// --- extensions to PROP
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet => {
+ let start = xml.create_dav_element("supported-report-set");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Property<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet(set) => {
+ let start = xml.create_dav_element("supported-report-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for v in set.iter() {
+ v.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for SupportedReport<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("supported-report");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for ReportName<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("report");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::VersionTree => {
+ let start = xml.create_dav_element("version-tree");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::ExpandProperty => {
+ let start = xml.create_dav_element("expand-property");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::Extension(ext) => ext.qwrite(xml).await?,
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// --- root REPORT object ---
+impl<E: Extension> QWrite for Report<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Report::VersionTree => unimplemented!(),
+ Report::ExpandProperty => unimplemented!(),
+ Report::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+// --- limit REPORT parameter ---
+impl QWrite for Limit {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("limit");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for NResults {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("nresults");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&format!("{}", self.0))))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) -> T {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ src.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let orig = NResults(100);
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let orig = Limit(NResults(1024));
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+}
diff --git a/aero-dav/src/versioningtypes.rs b/aero-dav/src/versioningtypes.rs
new file mode 100644
index 0000000..1f8d1cf
--- /dev/null
+++ b/aero-dav/src/versioningtypes.rs
@@ -0,0 +1,59 @@
+use super::types as dav;
+
+//@FIXME required for a full DAV implementation
+// See section 7.1 of the CalDAV RFC
+// It seems it's mainly due to the fact that the REPORT method is re-used.
+// https://datatracker.ietf.org/doc/html/rfc4791#section-7.1
+//
+// Defines (required by CalDAV):
+// - REPORT method
+// - expand-property root report method
+//
+// Defines (required by Sync):
+// - limit, nresults
+// - supported-report-set
+
+// This property identifies the reports that are supported by the
+// resource.
+//
+// <!ELEMENT supported-report-set (supported-report*)>
+// <!ELEMENT supported-report report>
+// <!ELEMENT report ANY>
+// ANY value: a report element type
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SupportedReportSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ SupportedReportSet(Vec<SupportedReport<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SupportedReport<E: dav::Extension>(pub ReportName<E>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportName<E: dav::Extension> {
+ VersionTree,
+ ExpandProperty,
+ Extension(E::ReportTypeName),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Report<E: dav::Extension> {
+ VersionTree, // Not yet implemented
+ ExpandProperty, // Not yet implemented
+ Extension(E::ReportType),
+}
+
+/// Limit
+/// <!ELEMENT limit (nresults) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Limit(pub NResults);
+
+/// NResults
+/// <!ELEMENT nresults (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct NResults(pub u64);
diff --git a/aero-dav/src/xml.rs b/aero-dav/src/xml.rs
new file mode 100644
index 0000000..e59f136
--- /dev/null
+++ b/aero-dav/src/xml.rs
@@ -0,0 +1,367 @@
+use futures::Future;
+use quick_xml::events::{BytesStart, Event};
+use quick_xml::name::ResolveResult;
+use quick_xml::reader::NsReader;
+use tokio::io::{AsyncBufRead, AsyncWrite};
+
+use super::error::ParsingError;
+
+// Constants
+pub const DAV_URN: &[u8] = b"DAV:";
+pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav";
+pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav";
+
+// Async traits
+pub trait IWrite = AsyncWrite + Unpin + Send;
+pub trait IRead = AsyncBufRead + Unpin;
+
+// Serialization/Deserialization traits
+pub trait QWrite {
+ fn qwrite(
+ &self,
+ xml: &mut Writer<impl IWrite>,
+ ) -> impl Future<Output = Result<(), quick_xml::Error>> + Send;
+}
+pub trait QRead<T> {
+ fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>;
+}
+
+// The representation of an XML node in Rust
+pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq + Clone + Sync;
+
+// ---------------
+
+/// Transform a Rust object into an XML stream of characters
+pub struct Writer<T: IWrite> {
+ pub q: quick_xml::writer::Writer<T>,
+ pub ns_to_apply: Vec<(String, String)>,
+}
+impl<T: IWrite> Writer<T> {
+ pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> {
+ self.create_ns_element("D", name)
+ }
+ pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> {
+ self.create_ns_element("C", name)
+ }
+
+ fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> {
+ let mut start = BytesStart::new(format!("{}:{}", ns, name));
+ if !self.ns_to_apply.is_empty() {
+ start.extend_attributes(
+ self.ns_to_apply
+ .iter()
+ .map(|(k, n)| (k.as_str(), n.as_str())),
+ );
+ self.ns_to_apply.clear()
+ }
+ start
+ }
+}
+
+/// Transform an XML stream of characters into a Rust object
+pub struct Reader<T: IRead> {
+ pub rdr: NsReader<T>,
+ cur: Event<'static>,
+ prev: Event<'static>,
+ parents: Vec<Event<'static>>,
+ buf: Vec<u8>,
+}
+impl<T: IRead> Reader<T> {
+ pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> {
+ let mut buf: Vec<u8> = vec![];
+ let cur = rdr.read_event_into_async(&mut buf).await?.into_owned();
+ let parents = vec![];
+ let prev = Event::Eof;
+ buf.clear();
+ Ok(Self {
+ cur,
+ prev,
+ parents,
+ rdr,
+ buf,
+ })
+ }
+
+ /// read one more tag
+ /// do not expose it publicly
+ async fn next(&mut self) -> Result<Event<'static>, ParsingError> {
+ let evt = self
+ .rdr
+ .read_event_into_async(&mut self.buf)
+ .await?
+ .into_owned();
+ self.buf.clear();
+ self.prev = std::mem::replace(&mut self.cur, evt);
+ Ok(self.prev.clone())
+ }
+
+ /// skip a node at current level
+ /// I would like to make this one private but not ready
+ pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> {
+ //println!("skipping inside node {:?} value {:?}", self.parents.last(), self.cur);
+ match &self.cur {
+ Event::Start(b) => {
+ let _span = self
+ .rdr
+ .read_to_end_into_async(b.to_end().name(), &mut self.buf)
+ .await?;
+ self.next().await
+ }
+ Event::End(_) => Err(ParsingError::WrongToken),
+ Event::Eof => Err(ParsingError::Eof),
+ _ => self.next().await,
+ }
+ }
+
+ /// check if this is the desired tag
+ fn is_tag(&self, ns: &[u8], key: &str) -> bool {
+ let qname = match self.peek() {
+ Event::Start(bs) | Event::Empty(bs) => bs.name(),
+ Event::End(be) => be.name(),
+ _ => return false,
+ };
+
+ let (extr_ns, local) = self.rdr.resolve_element(qname);
+
+ if local.into_inner() != key.as_bytes() {
+ return false;
+ }
+
+ match extr_ns {
+ ResolveResult::Bound(v) => v.into_inner() == ns,
+ _ => false,
+ }
+ }
+
+ pub fn parent_has_child(&self) -> bool {
+ matches!(self.parents.last(), Some(Event::Start(_)) | None)
+ }
+
+ fn ensure_parent_has_child(&self) -> Result<(), ParsingError> {
+ match self.parent_has_child() {
+ true => Ok(()),
+ false => Err(ParsingError::Recoverable),
+ }
+ }
+
+ pub fn peek(&self) -> &Event<'static> {
+ &self.cur
+ }
+
+ pub fn previous(&self) -> &Event<'static> {
+ &self.prev
+ }
+
+ // NEW API
+ pub async fn tag_string(&mut self) -> Result<String, ParsingError> {
+ self.ensure_parent_has_child()?;
+
+ let mut acc = String::new();
+ loop {
+ match self.peek() {
+ Event::CData(unescaped) => {
+ acc.push_str(std::str::from_utf8(unescaped.as_ref())?);
+ self.next().await?
+ }
+ Event::Text(escaped) => {
+ acc.push_str(escaped.unescape()?.as_ref());
+ self.next().await?
+ }
+ Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc),
+ _ => self.next().await?,
+ };
+ }
+ }
+
+ pub async fn maybe_read<N: Node<N>>(
+ &mut self,
+ t: &mut Option<N>,
+ dirty: &mut bool,
+ ) -> Result<(), ParsingError> {
+ if !self.parent_has_child() {
+ return Ok(());
+ }
+
+ match N::qread(self).await {
+ Ok(v) => {
+ *t = Some(v);
+ *dirty = true;
+ Ok(())
+ }
+ Err(ParsingError::Recoverable) => Ok(()),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn maybe_push<N: Node<N>>(
+ &mut self,
+ t: &mut Vec<N>,
+ dirty: &mut bool,
+ ) -> Result<(), ParsingError> {
+ if !self.parent_has_child() {
+ return Ok(());
+ }
+
+ match N::qread(self).await {
+ Ok(v) => {
+ t.push(v);
+ *dirty = true;
+ Ok(())
+ }
+ Err(ParsingError::Recoverable) => Ok(()),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> {
+ self.ensure_parent_has_child()?;
+
+ loop {
+ // Try parse
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise,
+ }
+
+ // If recovered, skip the element
+ self.skip().await?;
+ }
+ }
+
+ pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> {
+ // We can't find anything inside a self-closed tag
+ if !self.parent_has_child() {
+ return Ok(None);
+ }
+
+ loop {
+ // Try parse
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Some),
+ }
+
+ // Skip or stop
+ match self.peek() {
+ Event::End(_) => return Ok(None),
+ _ => self.skip().await?,
+ };
+ }
+ }
+
+ pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> {
+ let mut acc = Vec::new();
+ if !self.parent_has_child() {
+ return Ok(acc);
+ }
+
+ loop {
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => match self.peek() {
+ Event::End(_) => return Ok(acc),
+ _ => {
+ self.skip().await?;
+ }
+ },
+ Ok(v) => acc.push(v),
+ Err(e) => return Err(e),
+ }
+ }
+ }
+
+ pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> {
+ //println!("try open tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Empty(_) if self.is_tag(ns, key) => {
+ // hack to make `prev_attr` works
+ // here we duplicate the current tag
+ // as in other words, we virtually moved one token
+ // which is useful for prev_attr and any logic based on
+ // self.prev + self.open() on empty nodes
+ self.prev = self.cur.clone();
+ self.cur.clone()
+ }
+ Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
+ _ => return Err(ParsingError::Recoverable),
+ };
+
+ //println!("open tag {:?}", evt);
+ self.parents.push(evt.clone());
+ Ok(evt)
+ }
+
+ pub async fn open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Event<'static>, ParsingError> {
+ //println!("try open start tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
+ _ => return Err(ParsingError::Recoverable),
+ };
+
+ //println!("open start tag {:?}", evt);
+ self.parents.push(evt.clone());
+ Ok(evt)
+ }
+
+ pub async fn maybe_open(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Option<Event<'static>>, ParsingError> {
+ match self.open(ns, key).await {
+ Ok(v) => Ok(Some(v)),
+ Err(ParsingError::Recoverable) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn maybe_open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Option<Event<'static>>, ParsingError> {
+ match self.open_start(ns, key).await {
+ Ok(v) => Ok(Some(v)),
+ Err(ParsingError::Recoverable) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub fn prev_attr(&self, attr: &str) -> Option<String> {
+ match &self.prev {
+ Event::Start(bs) | Event::Empty(bs) => match bs.try_get_attribute(attr) {
+ Ok(Some(attr)) => attr
+ .decode_and_unescape_value(&self.rdr)
+ .ok()
+ .map(|v| v.into_owned()),
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+
+ // find stop tag
+ pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> {
+ //println!("close tag {:?}", self.parents.last());
+
+ // Handle the empty case
+ if !self.parent_has_child() {
+ self.parents.pop();
+ return self.next().await;
+ }
+
+ // Handle the start/end case
+ loop {
+ match self.peek() {
+ Event::End(_) => {
+ self.parents.pop();
+ return self.next().await;
+ }
+ _ => self.skip().await?,
+ };
+ }
+ }
+}