diff --git a/Cargo.lock b/Cargo.lock index b6fa27c..757345d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -97,7 +97,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", +] + +[[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.48", ] [[package]] @@ -106,6 +117,51 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[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", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "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", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -121,6 +177,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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.1" @@ -174,6 +242,57 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "env_logger" version = "0.10.1" @@ -187,6 +306,12 @@ dependencies = [ "termcolor", ] +[[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" @@ -197,6 +322,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[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 = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.29" @@ -253,7 +408,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -292,6 +447,17 @@ dependencies = [ "slab", ] +[[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.0" @@ -304,6 +470,43 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[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", + "indexmap 2.2.2", + "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 = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -316,12 +519,82 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +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", + "pin-project-lite", +] + +[[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", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -345,6 +618,52 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "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 = "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.9" @@ -356,6 +675,15 @@ dependencies = [ "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 = "itoa" version = "1.0.9" @@ -371,6 +699,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.150" @@ -389,12 +723,24 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -415,6 +761,66 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mockall_double" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.1" @@ -423,7 +829,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -460,6 +866,32 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -472,6 +904,48 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[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 = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty-hex" version = "0.4.0" @@ -480,22 +954,85 @@ checksum = "23c6b968ed37d62e35b4febaba13bfa231b0b7929d68b8a94e65445a17e2d35f" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[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 = "regex" version = "1.10.2" @@ -556,7 +1093,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.48", "unicode-ident", ] @@ -581,13 +1118,19 @@ version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.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.15" @@ -617,7 +1160,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -640,6 +1183,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + [[package]] name = "socket2" version = "0.5.5" @@ -652,15 +1201,32 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +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.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 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 = "termcolor" version = "1.4.0" @@ -670,6 +1236,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.50" @@ -687,9 +1259,54 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", +] + +[[package]] +name = "time" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe80ced77cbfb4cb91a94bf72b378b4b6791a0d9b7f09d0be747d1bdff4e68bd" +dependencies = [ + "deranged", + "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.34.0" @@ -707,6 +1324,16 @@ dependencies = [ "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", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.2.0" @@ -715,7 +1342,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -729,12 +1356,216 @@ dependencies = [ "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-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[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", + "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 = [ + "pin-project-lite", + "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.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "trust-dns-client" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14135e72c7e6d4c9b6902d4437881a8598f0145dbb2e3f86f92dbad845b61e63" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "once_cell", + "radix_trie", + "rand", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-proto" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[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 = "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" @@ -762,7 +1593,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -784,7 +1615,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1029,5 +1860,32 @@ version = "0.1.1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", +] + +[[package]] +name = "zvt_feig_terminal" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "env_logger", + "futures", + "log", + "mockall", + "mockall_double", + "num-traits", + "pin-project", + "serde", + "serde_json", + "thiserror", + "time", + "time-macros", + "tokio", + "tokio-stream", + "tonic", + "trust-dns-client", + "trust-dns-proto", + "zvt", ] diff --git a/Cargo.toml b/Cargo.toml index 440badc..f16ea55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "zvt_builder", "zvt_cli", "zvt_derive", + "zvt_feig_terminal", ] [workspace.package] diff --git a/WORKSPACE b/WORKSPACE index a273a56..fe4176d 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -24,6 +24,7 @@ crates_repository( "//zvt_builder:Cargo.toml", "//zvt_cli:Cargo.toml", "//zvt_derive:Cargo.toml", + "//zvt_feig_terminal:Cargo.toml", ], ) diff --git a/zvt_feig_terminal/BUILD.bazel b/zvt_feig_terminal/BUILD.bazel new file mode 100644 index 0000000..e842e65 --- /dev/null +++ b/zvt_feig_terminal/BUILD.bazel @@ -0,0 +1,17 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@crate_index//:defs.bzl", "all_crate_deps") + +rust_library( + name = "zvt_feig_terminal", + srcs = glob(["src/*.rs"]), + deps = all_crate_deps() + ["//zvt"], + proc_macro_deps = all_crate_deps(proc_macro = True), + edition = "2021", + visibility = ["//visibility:public"], +) + +rust_test( + name = "zvt_feig_terminal_test", + deps = all_crate_deps(normal_dev = True), + crate = ":zvt_feig_terminal", +) diff --git a/zvt_feig_terminal/Cargo.toml b/zvt_feig_terminal/Cargo.toml new file mode 100644 index 0000000..d08027a --- /dev/null +++ b/zvt_feig_terminal/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "zvt_feig_terminal" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +zvt = { version = "0.1.0", path = "../zvt" } +anyhow = "1.0.75" +tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread", "net", "sync"] } +log = "0.4.20" +trust-dns-client = "0.23.0" +trust-dns-proto = "0.23.0" +async-trait = "0.1.73" +env_logger = "0.10.0" +tonic = "0.8.3" +tokio-stream = "0.1.14" +serde = { version = "1.0.188", features = ["derive"] } +thiserror = "1.0.48" +time = { version = "0.3.28", features = ["macros"] } +time-macros = "0.2.14" +pin-project = "1.1.3" +futures = "0.3.28" +async-stream = "0.3.5" +mockall = "0.11.4" +mockall_double = "0.3.0" +num-traits = "0.2.17" + +[dev-dependencies] +serde_json = "1.0.105" diff --git a/zvt_feig_terminal/README.md b/zvt_feig_terminal/README.md new file mode 100644 index 0000000..3a8d705 --- /dev/null +++ b/zvt_feig_terminal/README.md @@ -0,0 +1,9 @@ +# ZVT Feig Terminal + +The crate implements the application logic for using a Feig terminal in production. + +We assume that you interact with the Feig terminal over TCP/IP. + +The high level interface for interacting with the Feig terminal is implemented +in [src/feig.rs](src/feig.rs). It provides good defaults for reading cards and +allows you to begin, commit or cancel transactions. diff --git a/zvt_feig_terminal/src/config.rs b/zvt_feig_terminal/src/config.rs new file mode 100644 index 0000000..7901be8 --- /dev/null +++ b/zvt_feig_terminal/src/config.rs @@ -0,0 +1,137 @@ +use anyhow::{bail, Result}; +use serde::Deserialize; +use std::net::Ipv4Addr; + +/// The config for the Feig terminal included in the +/// [PoleConfiguration::configuration]. +#[derive(serde::Deserialize, PartialEq, Debug, Clone)] +pub struct FeigConfig { + /// The currency code as defined by ISO 4217. See + /// https://en.wikipedia.org/wiki/ISO_4217. + /// + /// The input is the string representation of the currency as defined by + /// ISO 4217, e.x. `EUR` or `GBP`. + #[serde(default = "currency")] + #[serde(deserialize_with = "deserialize_iso_4217")] + pub currency: usize, + + /// The pre-authorization amount in the smallest currency unit (e.x. Cent). + #[serde(default = "pre_authorization_amount")] + pub pre_authorization_amount: usize, + + /// The default time to wait for reading a card in seconds. While a card is being read, the + /// payment terminal cannot do anything else (like refunding a transaction for example). + #[serde(default = "read_card_timeout")] + pub read_card_timeout: u8, + + /// The password to the payment terminal. + #[serde(default)] + pub password: usize, +} + +/// Deserializer which consumes a string code and returns the numerical code. +fn deserialize_iso_4217<'de, D>(deserializer: D) -> std::result::Result +where + D: serde::Deserializer<'de>, +{ + let code = String::deserialize(deserializer)?; + iso_4217(&code).map_err(serde::de::Error::custom) +} + +/// The default currency (returns the EUR code). +const fn currency() -> usize { + 978 +} + +/// The default read card timeout in seconds +const fn read_card_timeout() -> u8 { + 15 +} + +/// The default pre-authorization amount in Cent (returns 25 EUR). +const fn pre_authorization_amount() -> usize { + 2500 +} + +impl Default for FeigConfig { + fn default() -> Self { + Self { + currency: currency(), + pre_authorization_amount: pre_authorization_amount(), + read_card_timeout: read_card_timeout(), + password: 0, + } + } +} + +/// Maps the currency code (three letters) to a numeric value. +/// +/// The mapping is defined under the ISO 4217. See +/// https://en.wikipedia.org/wiki/ISO_4217 +fn iso_4217(code: &str) -> Result { + match code.to_uppercase().as_str() { + // Keep the list sorted by the numeric value. + "SEK" => Ok(752), + "GBP" => Ok(826), + "EUR" => Ok(978), + _ => bail!("Unknown currency code {code}"), + } +} + +/// The configuration needed for the entire payment terminal, which contains +/// parsed data. +#[derive(Clone, Debug)] +pub struct Config { + pub terminal_id: String, + /// We only use feig_serial to make sure we are connected to the proper + /// terminal. + pub feig_serial: String, + pub ip_address: Ipv4Addr, + /// Parsed from [PoleConfiguration::configuration]. + pub feig_config: FeigConfig, + /// Maximum number of concurrent transactions. + pub transactions_max_num: usize, +} + +impl Default for Config { + fn default() -> Self { + Self { + terminal_id: String::default(), + feig_serial: String::default(), + ip_address: Ipv4Addr::new(0, 0, 0, 0), + feig_config: FeigConfig::default(), + transactions_max_num: 1, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_feig_config() { + // Valid inputs. + let empty = serde_json::from_str::("{}").unwrap(); + assert_eq!(empty, FeigConfig::default()); + + let with_currency = serde_json::from_str::("{\"currency\": \"GBP\"}").unwrap(); + assert_eq!(with_currency.currency, 826); + assert_eq!(with_currency.pre_authorization_amount, 2500); + + let with_all = serde_json::from_str::( + "{\"currency\": \"GBP\", \"pre_authorization_amount\": 10}", + ) + .unwrap(); + assert_eq!(with_all.currency, 826); + assert_eq!(with_all.pre_authorization_amount, 10); + + // Invalid inputs. + assert!(serde_json::from_str::("{\"currency\": \"ABC\"}").is_err()); + assert!(serde_json::from_str::("{\"currency\": 123}").is_err()); + assert!( + serde_json::from_str::("{\"pre_authorization_amount\": \"AB\"}").is_err() + ); + } +} diff --git a/zvt_feig_terminal/src/feig.rs b/zvt_feig_terminal/src/feig.rs new file mode 100644 index 0000000..d7afa8a --- /dev/null +++ b/zvt_feig_terminal/src/feig.rs @@ -0,0 +1,569 @@ +use crate::config::Config; +use crate::stream::{ResetSequence, TcpStream}; +use anyhow::{anyhow, bail, Result}; +use log::{info, warn}; +use num_traits::FromPrimitive; +use std::collections::HashMap; +use std::time::Duration; +use tokio_stream::StreamExt; +use zvt::{constants, feig, packets, sequences}; + +/// The card information returned from read-card. +pub enum CardInfo { + /// Indicatates if we've received a bank card. + Bank, + + /// Indicates if we've received a member ship card. The stirng is our tag-id. + MembershipCard(String), +} + +/// Summary after a transaction. +pub struct TransactionSummary { + /// The terminal-id of the payment terminal. + pub terminal_id: Option, + + /// The amount billed in the transaction, in Cents. + pub amount: Option, + + /// The trace number identifying the transaction. + pub trace_number: Option, + + /// The date of the transaction, 4 numerical letters, e.g. -> '0517' means + /// the payment was made on May 17th. + pub date: Option, + + /// The time of the payment - 6 numerical letters, e.g. '134530' means the + /// payment was made at 13:45:30. + pub time: Option, +} + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum Error { + #[error("Unexpected packet")] + UnexpectedPacket, + + #[error("Active transaction: {0}")] + ActiveTransaction(String), + + #[error("No Card presented")] + NoCardPresented, + + #[error("Unknown token: {0}")] + UnknownToken(String), + + #[error("The presented card requires a PIN entry.")] + NeedsPinEntry, +} + +/// Default card type, which is chip-card, as defined in Table 6. +const CARD_TYPE: Option = Some(0x10); + +/// Default value for reading control. +/// +/// See Tlv tag 0x1f15 for the documentation. +const SHORT_CARD_READING_CONTROL: Option = Some(0xd0); + +/// Default value for allowed card types. +/// +/// See Tlv tag 0x1f60 for documentation. +const ALLOWED_CARDS: Option = Some(0x07); + +/// Default dialog control for reading the card. +/// +/// The values are defined in Table 7 - having only the choice between 1 and 0; +/// However, the value 2 can silence the beeps. +const DIALOG_CONTROL: Option = Some(0x02); + +/// The default payment type, defined in table 4. +/// +/// Payment type according to PTs decision excluding `GeldKarte`. +const PAYMENT_TYPE: Option = Some(0x40); + +/// Identifier for the individual reference number. +/// +/// This identifier is used in BMP60 when transmitting the individual reference +/// number to the host. It allows us to tack payments at Lavego. +const BMP_PREFIX: &str = "AC"; + +#[derive(Default)] +pub struct Feig { + socket: TcpStream, + /// Map of active transactions to their receipt-number. + transactions: HashMap, + + /// Maximum number of concurrent transactions. + transactions_max_num: usize, +} + +impl Feig { + pub async fn new(config: Config) -> Result { + let transactions_max_num = config.transactions_max_num; + let mut this = Self { + socket: TcpStream::new(config)?, + transactions: HashMap::new(), + transactions_max_num, + }; + + // Ignore the errors from configure (call fails if e.x. the terminal id is + // invalid) + let _ = this.configure().await; + Ok(this) + } + + /// Returns the system information of the feig-terminal. + async fn get_system_info( + &mut self, + ) -> Result { + let request = feig::packets::CVendFunctions { + password: None, + instr: 1, + }; + let mut stream = feig::sequences::GetSystemInfo::into_stream(request, &mut self.socket); + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + feig::sequences::GetSystemInfoResponse::CVendFunctionsEnhancedSystemInformationCompletion(packet) => { + return Ok(packet) + }, + feig::sequences::GetSystemInfoResponse::Abort(packet) => bail!(zvt::ZVTError::Aborted(packet.error)) + } + } + bail!(zvt::ZVTError::IncompleteData) + } + + /// Sets the terminal id. + /// + /// Function does nothing if the feig-terminal has already the desired + /// terminal-id. + async fn set_terminal_id(&mut self) -> Result<()> { + let system_info = self.get_system_info().await?; + let config = self.socket.config(); + + // Set the terminal id if required. + if config.terminal_id == system_info.terminal_id { + info!("Terminal id already up-to-date"); + return Ok(()); + } + + // Sadly the terminal_id is a int, but we communicate it as a string... + let terminal_id = config.terminal_id.parse::()?; + let request = packets::SetTerminalId { + password: config.feig_config.password, + terminal_id: Some(terminal_id), + }; + + info!("Updating the terminal_id to {terminal_id}"); + + let mut stream = sequences::SetTerminalId::into_stream(request, &mut self.socket); + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::SetTerminalIdResponse::CompletionData(_) => return Ok(()), + sequences::SetTerminalIdResponse::Abort(data) => { + bail!(zvt::ZVTError::Aborted(data.error)) + } + } + } + + bail!(zvt::ZVTError::IncompleteData) + } + + /// Initializes the feig-terminal. + async fn initialize(&mut self) -> Result<()> { + let password = self.socket.config().feig_config.password; + let request = packets::Initialization { password }; + let mut stream = sequences::Initialization::into_stream(request, &mut self.socket); + while let Some(response) = stream.next().await { + use sequences::InitializationResponse::*; + let Ok(response) = response else { + continue; + }; + match response { + IntermediateStatusInformation(_) => (), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + CompletionData(_) => return Ok(()), + Abort(data) => { + bail!(zvt::ZVTError::Aborted(data.error)) + } + } + } + + bail!(zvt::ZVTError::IncompleteData) + } + + /// Returns the pending transaction. + /// + /// We return a vector of possible pending transactions. Right now we just + /// check for one. + async fn get_pending(&mut self) -> Result> { + let request = packets::PartialReversal { + receipt_no: Some(0xFFFF), + ..packets::PartialReversal::default() + }; + + let mut stream = sequences::PartialReversal::into_stream(request, &mut self.socket); + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::PartialReversalResponse::PartialReversalAbort(data) => { + // The 0xFFFF means no pending transactions. + let Some(receipt_no) = data.receipt_no else { + return Ok(vec![]); + }; + + if receipt_no == 0xFFFF { + return Ok(vec![]); + } + return Ok(vec![receipt_no]); + } + _ => bail!(Error::UnexpectedPacket), + } + } + + bail!(zvt::ZVTError::IncompleteData) + } + + /// Cancels all pending transactions. + async fn cancel_pending(&mut self) -> Result<()> { + self.transactions.clear(); + let pending = self.get_pending().await?; + for p in pending { + self.cancel_transaction_by_receipt_no(p).await? + } + Ok(()) + } + + /// Runs an end-of-day job. + /// + /// Will first cancel all currently pending transactions and then run an + /// end of day job. Caution: Calling this will wipe all ongoing + /// transactions. + async fn end_of_day(&mut self) -> Result<()> { + // Cancel all possible pending transactions. + self.cancel_pending().await?; + + let password = self.socket.config().feig_config.password; + let request = packets::EndOfDay { password }; + let mut stream = sequences::EndOfDay::into_stream(request, &mut self.socket); + // Note: The timeout might be too little as this needs a call to the + // PT's host. + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::EndOfDayResponse::CompletionData(_) => return Ok(()), + sequences::EndOfDayResponse::Abort(data) => { + // If the payment terminal was not configured it may return + // 'receiver not ready' - in this case we'll ignore the + // error. + if data.error == constants::ErrorMessages::ReceiverNotReady as u8 { + warn!("End-of-Day: Terminal not ready"); + return Ok(()); + } + + bail!(zvt::ZVTError::Aborted(data.error)) + } + _ => {} + } + } + + bail!(zvt::ZVTError::IncompleteData) + } + + /// Initializes the connection. + /// + /// We're doing the following + /// * Set the terminal id if required. + /// * Initialize the terminal. + /// * Run end-of-day job. + pub async fn configure(&mut self) -> Result<()> { + self.set_terminal_id().await?; + self.initialize().await?; + self.end_of_day().await?; + + Ok(()) + } + + /// Reads the card data + /// + /// The call will either return some [CardInfo] or [None] - if there is no + /// card presented during the specified [config.read_card_timeout]. + pub async fn read_card(&mut self) -> Result { + let timeout_sec = self.socket.config().feig_config.read_card_timeout; + let request = packets::ReadCard { + timeout_sec, + card_type: CARD_TYPE, + dialog_control: DIALOG_CONTROL, + tlv: Some(packets::tlv::ReadCard { + card_reading_control: SHORT_CARD_READING_CONTROL, + card_type: ALLOWED_CARDS, + }), + }; + + let retry = futures::stream::repeat(()) + .throttle(Duration::from_secs(2)) + .take(20); + let mut stream = sequences::ReadCard::into_stream_with_retry( + request, + &mut self.socket, + retry, + Duration::from_secs((timeout_sec + 2) as u64), + ); + let mut card_info = None; + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::ReadCardResponse::Abort(data) => { + use zvt::constants::ErrorMessages::*; + + let err = zvt::constants::ErrorMessages::from_u8(data.error) + .ok_or(anyhow!("Unknown error code: 0x{:X}", data.error))?; + match err { + AbortViaTimeoutOrAbortKey => { + // If there is no card to read, we will receive a timeout + // error. + bail!(Error::NoCardPresented) + } + other => bail!("Unhandled error: {other}"), + } + } + sequences::ReadCardResponse::StatusInformation(data) => { + // Retrieve the card information. + let tlv = data.tlv.ok_or(zvt::ZVTError::IncompleteData)?; + if !tlv.subs.is_empty() { + let subs = &tlv.subs[0]; + if subs.application_id.is_some() { + card_info = Some(CardInfo::Bank); + } else { + bail!("Unknown card type") + } + } else if let Some(mut uuid) = tlv.uuid { + uuid = uuid.to_uppercase(); + if uuid.len() > 14 { + uuid = uuid[uuid.len() - 14..].to_string(); + uuid = uuid.strip_prefix("000000").unwrap_or(&uuid).to_string(); + } + + card_info = Some(CardInfo::MembershipCard(uuid)); + } else { + bail!(zvt::ZVTError::IncompleteData) + } + } + _ => {} + } + } + + card_info.ok_or(zvt::ZVTError::IncompleteData.into()) + } + + /// Begins a transaction. + /// + /// The transaction must be finished with either [Feig::cancel_transaction] + /// or [Feig::commit_transaction]. The given `transaction` must be used + /// for both follow up functions. The method returns an error if the + /// maximum number of currently active transactions has been reached or if + /// the [Transaction::token] is already in use. + /// + /// Under the hood the method maps to [sequences::Reservation]. + /// + /// # Arguments + /// * `token` - The token to identify the transaction with. + pub async fn begin_transaction(&mut self, token: &str) -> Result<()> { + if self.transactions.len() == self.transactions_max_num { + bail!(Error::ActiveTransaction(format!( + "Maximum number of transactions reached: {}", + self.transactions_max_num + ))) + } + + if self.transactions.contains_key(token) { + bail!(Error::ActiveTransaction("Token already in use".to_string())) + } + + let config = self.socket.config(); + let request = packets::Reservation { + currency: Some(config.feig_config.currency), + amount: Some(config.feig_config.pre_authorization_amount), + payment_type: PAYMENT_TYPE, + tlv: Some(packets::tlv::PreAuthData { + bmp_data: Some(packets::tlv::Bmp60 { + bmp_prefix: BMP_PREFIX.to_string(), + bmp_data: token.to_string(), + }), + }), + ..packets::Reservation::default() + }; + + let mut stream = sequences::Reservation::into_stream(request, &mut self.socket); + let mut receipt_no = None; + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::AuthorizationResponse::Abort(data) => { + use zvt::constants::ErrorMessages::*; + + let err = zvt::constants::ErrorMessages::from_u8(data.error) + .ok_or(anyhow!("Unknown error code: 0x{:X}", data.error))?; + match err { + NecessaryDeviceNotPresentOrDefective => { + bail!(Error::NeedsPinEntry) + } + _ => bail!(zvt::ZVTError::Aborted(data.error)), + } + } + sequences::AuthorizationResponse::StatusInformation(data) => { + // Only overwrite the receipt_no if it is contained in the + // message. + if let Some(inner) = data.receipt_no { + receipt_no = Some(inner); + } + } + _ => {} + } + } + + match receipt_no { + None => bail!(zvt::ZVTError::IncompleteData), + Some(receipt_no) => { + self.transactions.insert(token.to_string(), receipt_no); + Ok(()) + } + } + } + + async fn cancel_transaction_by_receipt_no(&mut self, receipt_no: usize) -> Result<()> { + let config = self.socket.config(); + let request = packets::PreAuthReversal { + payment_type: PAYMENT_TYPE, + currency: Some(config.feig_config.currency), + receipt_no: Some(receipt_no), + }; + + let mut stream = sequences::PreAuthReversal::into_stream(request, &mut self.socket); + while let Some(response) = stream.next().await { + let Ok(response) = response else { + continue; + }; + match response { + sequences::PartialReversalResponse::CompletionData(_) => return Ok(()), + sequences::PartialReversalResponse::PartialReversalAbort(data) => { + bail!(zvt::ZVTError::Aborted(data.error)) + } + _ => {} + } + } + + bail!(zvt::ZVTError::IncompleteData) + } + + /// Cancels a transaction. + /// + /// The transaction must be started with [Feig::begin_transaction] and the + /// argument must contain a [Transaction::token] matching the token from + /// [Feig::begin_transaction]. The method fails if the `token` is unknown. + /// + /// # Arguments + /// * `token` - The token the transaction is identified with. + pub async fn cancel_transaction(&mut self, token: &str) -> Result<()> { + // Check if the transaction is known to us. + let removed = self.transactions.remove(token); + match removed { + None => bail!(Error::UnknownToken(token.to_string())), + Some(receipt_no) => { + self.cancel_transaction_by_receipt_no(receipt_no).await?; + + // Run end of day if we don't have any pending transactions + if self.transactions.is_empty() { + self.end_of_day().await?; + } + Ok(()) + } + } + } + + /// Commits a transaction. + /// + /// The transaction must be started with [Feig::begin_transaction] and the + /// argument must contain a [Transaction::token] matching the token from + /// [Feig::begin_transaction]. The method fails if the `token` is unknown. + /// + /// # Arguments + /// * `token` - The token under which the transaction is known. + /// * `amount` - The amount in fractional monetary unit. + /// + /// # Returns + /// The summary of the transaction. + pub async fn commit_transaction( + &mut self, + token: &str, + amount: u64, + ) -> Result { + let removed = self.transactions.remove(token); + if removed.is_none() { + bail!(Error::UnknownToken(token.to_string())); + } + + let config = self.socket.config(); + let reversal_amount = config + .feig_config + .pre_authorization_amount + .saturating_sub(amount as usize); + + let request = packets::PartialReversal { + receipt_no: Some(removed.unwrap()), + currency: Some(config.feig_config.currency), + amount: Some(reversal_amount), + payment_type: PAYMENT_TYPE, + tlv: Some(packets::tlv::PreAuthData { + bmp_data: Some(packets::tlv::Bmp60 { + bmp_prefix: BMP_PREFIX.to_string(), + bmp_data: token.to_string(), + }), + }), + }; + + let mut stream = sequences::PartialReversal::into_stream(request, &mut self.socket); + let mut status_information = None; + while let Some(response) = stream.next().await { + use sequences::PartialReversalResponse::*; + let Ok(response) = response else { + continue; + }; + match response { + IntermediateStatusInformation(_) | CompletionData(_) => (), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + StatusInformation(data) => status_information = Some(data), + PartialReversalAbort(data) => bail!(zvt::ZVTError::Aborted(data.error)), + } + } + drop(stream); + + if self.transactions.is_empty() { + self.end_of_day().await?; + } + + let status_information = status_information.ok_or(zvt::ZVTError::IncompleteData)?; + Ok(TransactionSummary { + terminal_id: status_information + .terminal_id + .map(|inner| inner.to_string()), + date: status_information.date.map(|n| format!("{:04}", n)), + time: status_information.time.map(|n| format!("{:06}", n)), + amount: status_information.amount.map(|inner| inner as u64), + trace_number: status_information.trace_number.map(|inner| inner as u64), + }) + } +} diff --git a/zvt_feig_terminal/src/lib.rs b/zvt_feig_terminal/src/lib.rs new file mode 100644 index 0000000..0573c8d --- /dev/null +++ b/zvt_feig_terminal/src/lib.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod feig; +pub mod stream; diff --git a/zvt_feig_terminal/src/stream.rs b/zvt_feig_terminal/src/stream.rs new file mode 100644 index 0000000..3076fea --- /dev/null +++ b/zvt_feig_terminal/src/stream.rs @@ -0,0 +1,488 @@ +//! Reconnecting TcpStream. +//! +//! We observe that we need to reset the connection on errors resulting either +//! from transmission or parsing. The module implements a [TcpStream], which +//! it self contains all information to reset a tcp connection and a wrapper +//! around [sequence::Sequence] - which is responsible for restarting a +//! dropped connection and terminating it on errors.s +use crate::config::Config; + +use anyhow::{bail, Result}; +use async_stream::stream; +use futures::stream::BoxStream; +use log::{debug, info, warn}; +use std::io::{Error, ErrorKind}; +use std::net::SocketAddrV4; +use std::time::Duration; +use tokio_stream::StreamExt; +use zvt::{encoding, feig, io, packets, sequences, sequences::Sequence}; + +const TIMEOUT: Duration = Duration::from_secs(60); + +/// The implementation of our I/O. +/// +/// We're using a custom switch very similar to what [mockall_double::double] +/// is doing. +#[cfg(not(test))] +type InnerTcpStream = tokio::net::TcpStream; + +/// Mocked I/O for unit tests. +#[cfg(test)] +type InnerTcpStream = test::MockTcpStream; + +/// The modules just to use the mocking. Really cumbersome... +mod outer { + use super::*; + #[mockall::automock()] + pub(super) mod inner { + use super::*; + + /// Reconnection + /// + /// Tries to open a new [InnerTcpStream] to the PT defined in the + /// `config` and performs basic registration to the terminal. + /// + /// We mock this function in the test configuration. + #[cfg_attr(test, allow(dead_code))] + pub async fn connect(config: &Config) -> Result> { + // Configuration byte. + pub const CONFIG_BYTE: u8 = 0xde; + + let source = + InnerTcpStream::connect(SocketAddrV4::new(config.ip_address, 22000)).await?; + let mut socket = io::PacketTransport { source }; + + let request = packets::Registration { + password: config.feig_config.password, + config_byte: CONFIG_BYTE, + currency: Some(config.feig_config.currency), + tlv: None, + }; + + // Register to the terminal. + let mut stream = + ::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + let completion = response?; + info!("Registered to the terminal {:?}", completion); + } + drop(stream); + + // Verify that we're connected to the right terminal. + let request = feig::packets::CVendFunctions { + password: None, + instr: 1, + }; + let mut stream = + ::into_stream(&request, &mut socket); + let Some(packet) = stream.next().await else { + bail!(zvt::ZVTError::IncompleteData) + }; + debug!("Received {packet:?}"); + match packet? { + feig::sequences::GetSystemInfoResponse::CVendFunctionsEnhancedSystemInformationCompletion(packet) => { + if packet.device_id.to_lowercase() == config.feig_serial.to_lowercase() { + drop(stream); + return Ok(socket); + } + bail!(Error::new(ErrorKind::NotConnected, "Wrong device")) + }, + feig::sequences::GetSystemInfoResponse::Abort(packet) => bail!(zvt::ZVTError::Aborted(packet.error)) + } + } + } +} + +#[mockall_double::double] +use outer::inner; + +/// Stream which can reconnect. +#[derive(Default)] +pub struct TcpStream { + /// Configuration, needed to regain connection. + config: Config, + /// Underlying I/O. + inner: Option>, +} + +impl TcpStream { + /// Creates a new [TcpStream]. + /// + /// # Arguments + /// * `pole_config` - The configuration from backend. + pub fn new(mut config: Config) -> Result { + // Check the terminal_id + if config.terminal_id.is_empty() { + warn!("No terminal-id provided. Fix this if it's production"); + config.terminal_id = "00000000".to_string(); + } + + Ok(Self { + config, + inner: None, + }) + } + + /// Returns the config used to construct the stream. + pub fn config(&self) -> &Config { + &self.config + } +} + +/// One of our most important. +/// +/// The wrapper around the [sequence::Sequence], which also manages the tcp +/// connection. Before starting the stream, it will setup a connection (if +/// necessary) and tear it down on errors. +pub trait ResetSequence: Sequence +where + encoding::Default: encoding::Encoding, +{ + fn into_stream<'a>( + input: Self::Input, + src: &'a mut TcpStream, + ) -> BoxStream> + where + Self: 'a, + Self::Input: std::fmt::Debug, + Self::Output: std::fmt::Debug, + { + let repeater = futures::stream::repeat(()) + .throttle(std::time::Duration::from_secs(2)) + .take(20); + + return Self::into_stream_with_retry(input, src, repeater, TIMEOUT); + } + + fn into_stream_with_retry<'a, RetryStream>( + input: Self::Input, + src: &'a mut TcpStream, + retry: RetryStream, + timeout: std::time::Duration, + ) -> BoxStream> + where + Self: 'a, + Self::Input: std::fmt::Debug, + Self::Output: std::fmt::Debug, + RetryStream: futures::Stream + Send + 'a, + { + debug!("Sending packet {input:?}"); + let s = stream! { + + tokio::pin!(retry); + while let Some(()) = retry.next().await { + // We don't have a valid connection - we must reconnect. + if src.inner.is_none() { + warn!("Reconnecting"); + match inner::connect(&src.config).await { + Ok(inner) => src.inner = Some(inner), + Err(err) => { + warn!("Failed to reconnect: {err:?}"); + yield Err(err); + continue; + } + } + } + + // Start the underlying stream. + let mut stream = + ::into_stream(&input, src.inner.as_mut().unwrap()); + let mut is_err = false; + // We are awaiting packets with a timeout. In case of a timeout + // we convert the error into None, to break out of the loop. The + // timeout is needed since we may not finish the preceding + // sequence and hang. + while let Some(packet) = match tokio::time::timeout(timeout, stream.next()).await { + Ok(packet) => packet, + Err(_) => { + warn!("Timeout"); + is_err = true; + None + } + } { + debug!("Received packet {packet:?}"); + is_err = packet.is_err(); + yield packet; + if is_err { + break; + } + } + drop(stream); + if is_err { + debug!("Dropping the connection"); + src.inner = None; + continue; + } + return; + } + }; + + Box::pin(s) + } +} + +impl ResetSequence for St +where + St: Sequence, + encoding::Default: encoding::Encoding<::Input>, +{ +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::FeigConfig; + use std::pin::Pin; + use std::task::{Context, Poll}; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + use tokio::net::ToSocketAddrs; + + /// Mocked TcpStream. + pub struct MockTcpStream; + + impl MockTcpStream { + /// Interface for connecting - the same signature as + /// [tokio::net::TcpStream::connect]. + /// + /// The function is never called but just there to compile the code. + #[allow(dead_code)] + pub(super) async fn connect(_: A) -> std::io::Result + where + A: ToSocketAddrs, + { + unimplemented!("just a mock") + } + } + + /// Impl for AsyncRead - which is required for [tokio::AsyncReadExt], which + /// is used in the [sequence::Sequence] signature. + impl AsyncRead for MockTcpStream { + fn poll_read( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + unimplemented!("just a mock") + } + } + + /// Impl for AsyncRead - which is required for [tokio::AsyncWriteExt], which + /// is used in the [sequence::Sequence] signature.s + impl AsyncWrite for MockTcpStream { + fn poll_write( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: &[u8], + ) -> Poll> { + unimplemented!("just a mock") + } + + fn poll_flush( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll> { + unimplemented!("just a mock") + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll> { + unimplemented!("just a mock") + } + } + + /// Fake sequence which always fails. + struct FailSequence {} + + impl sequences::Sequence for FailSequence { + type Input = feig::packets::CVendFunctions; + type Output = feig::sequences::GetSystemInfoResponse; + + /// Fake Stream + /// + /// Returns just an error, without calling any I/O functions.s + fn into_stream<'a, Source>( + _: &'a Self::Input, + _: &'a mut io::PacketTransport, + ) -> std::pin::Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let res = vec![Err(zvt::ZVTError::NonImplemented.into())]; + Box::pin(futures::stream::iter(res)) + } + } + + /// Fake sequence which always succeeds. + struct SuccessSequence {} + + impl sequences::Sequence for SuccessSequence { + type Input = feig::packets::CVendFunctions; + type Output = feig::sequences::GetSystemInfoResponse; + + /// Fake Stream + /// + /// Returns just a successful message without calling any I/O. + fn into_stream<'a, Source>( + _: &'a Self::Input, + _: &'a mut io::PacketTransport, + ) -> std::pin::Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let res = vec![Ok(Self::Output::Abort(zvt::packets::Abort { error: 0 }))]; + Box::pin(futures::stream::iter(res)) + } + } + + /// Fake sequence which takes some time to return. + struct SlowSequence {} + + impl sequences::Sequence for SlowSequence { + type Input = feig::packets::CVendFunctions; + type Output = feig::sequences::GetSystemInfoResponse; + /// Slow stream + /// + /// Returns a message and waits for a long time before the next one. + fn into_stream<'a, Source>( + _: &'a Self::Input, + _: &'a mut io::PacketTransport, + ) -> std::pin::Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + Box::pin( + futures::stream::repeat_with(|| { + Ok(Self::Output::Abort(zvt::packets::Abort { error: 0 })) + }) + .throttle(Duration::from_secs(60)), + ) + } + } + + fn get_config() -> Config { + Config { + feig_config: FeigConfig { + currency: 0, + pre_authorization_amount: 0, + read_card_timeout: 15, + password: 123456, + }, + ..Config::default() + } + } + + #[tokio::test] + async fn test_running_once() { + // Test the connection failure at beginning. + let mut socket = TcpStream { + config: get_config(), + inner: None, + }; + + let ctx = inner::connect_context(); + ctx.expect().times(1).returning(|_| bail!("not connected")); + + let repeater = futures::stream::repeat(()).take(1); + let request = feig::packets::CVendFunctions { + password: None, + instr: 0, + }; + let mut stream = + FailSequence::into_stream_with_retry(request, &mut socket, repeater, TIMEOUT); + + assert!(stream.next().await.unwrap().is_err()); + assert!(stream.next().await.is_none()); + ctx.checkpoint(); + drop(stream); + // The inner should still be none. + assert!(socket.inner.is_none()); + + // Now pretend that the connection was successful. + ctx.expect().returning(|_| { + Ok(io::PacketTransport { + source: InnerTcpStream {}, + }) + }); + let repeater = futures::stream::repeat(()).take(1); + let request = feig::packets::CVendFunctions { + password: None, + instr: 0, + }; + let mut stream = + FailSequence::into_stream_with_retry(request, &mut socket, repeater, TIMEOUT); + + // Verify the results. + assert!(stream.next().await.unwrap().is_err()); + assert!(stream.next().await.is_none()); + drop(stream); + // The inner connection shall be resetted. + assert!(socket.inner.is_none()); + + // Now call the successful sequence. + let repeater = futures::stream::repeat(()).take(1); + let request = feig::packets::CVendFunctions { + password: None, + instr: 0, + }; + let mut stream = + SuccessSequence::into_stream_with_retry(request, &mut socket, repeater, TIMEOUT); + assert!(stream.next().await.unwrap().is_ok()); + assert!(stream.next().await.is_none()); + drop(stream); + assert!(socket.inner.is_some()); + ctx.checkpoint(); + + // Now try with retrying the fail sequence. + socket.inner = None; + let attempts = 2; + ctx.expect().times(attempts).returning(|_| { + Ok(io::PacketTransport { + source: InnerTcpStream {}, + }) + }); + + let repeater = futures::stream::repeat(()).take(attempts); + let request = feig::packets::CVendFunctions { + password: None, + instr: 0, + }; + let mut stream = + FailSequence::into_stream_with_retry(request, &mut socket, repeater, TIMEOUT); + + assert!(stream.next().await.unwrap().is_err()); + assert!(stream.next().await.unwrap().is_err()); + assert!(stream.next().await.is_none()); + ctx.checkpoint(); + drop(stream); + // The inner should still be none. + assert!(socket.inner.is_none()); + + // Now try with a timeout. + ctx.expect().returning(|_| { + Ok(io::PacketTransport { + source: InnerTcpStream {}, + }) + }); + let repeater = futures::stream::repeat(()).take(1); + let request = feig::packets::CVendFunctions { + password: None, + instr: 0, + }; + let mut stream = SlowSequence::into_stream_with_retry( + request, + &mut socket, + repeater, + Duration::from_millis(10), + ); + assert!(stream.next().await.unwrap().is_ok()); + assert!(stream.next().await.is_none()); + drop(stream); + // The inner should still be none. + assert!(socket.inner.is_none()); + } +}