diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-29 10:14:51 +0200 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-29 10:14:51 +0200 |
commit | b9ce5886033677f6c65a4b873e17574fdb8df31d (patch) | |
tree | 9ed1d721361027d7d6fef0ecad65d7e1b74a7ddb /aero-dav | |
parent | 0dcf69f180f5a7b71b6ad2ac67e4cdd81e5154f1 (diff) | |
parent | 5954de6efbb040b8b47daf0c7663a60f3db1da6e (diff) | |
download | aerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.tar.gz aerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.zip |
Merge branch 'caldav'
Diffstat (limited to 'aero-dav')
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="<" +entity_decimal="" +entity_external="&a;" +entity_hex="" + +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, ¶m_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>"fffff-abcd2"</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>"fffff-abcd3"</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 <, >, & 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 & Canada) + /// END:STANDARD + /// BEGIN:DAYLIGHT + /// DTSTART:19870405T020000 + /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 + /// TZOFFSETFROM:-0500 + /// TZOFFSETTO:-0400 + /// TZNAME:Eastern Daylight Time (US & 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 <, >, & 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 <, >, & 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>"zzyzx"</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?, + }; + } + } +} |