diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bde647 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/bazel-* diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c280ac7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,955 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 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 = "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 = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[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", +] + +[[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", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[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 = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +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 = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +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", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[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 = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[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", +] + +[[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", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[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.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[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.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "yore" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f58c7ce1b7faa85a5c5a8b45b428c89cabdcd097bb472b7668037b291a8a20b" +dependencies = [ + "thiserror", +] + +[[package]] +name = "zvt" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "chrono", + "clap", + "env_logger", + "futures", + "hex", + "log", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "yore", + "zvt_builder", + "zvt_derive", +] + +[[package]] +name = "zvt_builder" +version = "0.1.0" +dependencies = [ + "chrono", + "hex", + "log", + "thiserror", + "yore", + "zvt_derive", +] + +[[package]] +name = "zvt_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d97a1c0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "zvt", + "zvt_builder", + "zvt_derive", +] diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..5e07f5a --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,31 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "rules_rust", + sha256 = "814680e1ab535f799fd10e8739ddca901351ceb4d2d86dd8126c22d36e9fcbd9", + urls = ["https://github.com/bazelbuild/rules_rust/releases/download/0.29.0/rules_rust-v0.29.0.tar.gz"], +) + +load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_register_toolchains") + +rules_rust_dependencies() + +rust_register_toolchains() + +load("@rules_rust//crate_universe:defs.bzl", "crate", "crates_repository") + +crates_repository( + name = "crate_index", + cargo_lockfile = "//:Cargo.lock", + isolated = False, + manifests = [ + "//:Cargo.toml", + "//zvt:Cargo.toml", + "//zvt_builder:Cargo.toml", + "//zvt_derive:Cargo.toml", + ], +) + +load("@crate_index//:defs.bzl", "crate_repositories") + +crate_repositories() diff --git a/py/dump_packages.py b/py/dump_packages.py new file mode 100755 index 0000000..918723a --- /dev/null +++ b/py/dump_packages.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import argparse +import binascii +import os +import pyshark + +def parse_args(): + p = argparse.ArgumentParser(description= + "Extract ZVT packages from the given file." + ) + + p.add_argument("-i", "--input", type=Path, required=True, help = "input file") + p.add_argument("-o", "--output_dir", type=Path, default="zvt_packets", help = "Directory for dumped files") + + return p.parse_args() + +def main(): + args = parse_args() + + if not os.path.exists(args.output_dir): + os.makedirs(args.output_dir) + + cap = pyshark.FileCapture(args.input, display_filter='tcp.port==22000') + for pkt in cap: + if not hasattr(pkt.tcp, 'payload'): + continue # Skip packets without a payload + + timestamp = pkt.sniff_timestamp + src = pkt.ip.src + dst = pkt.ip.dst + payload = pkt.tcp.payload + + binary_payload = binascii.unhexlify(payload.replace(':', '')) + + output_filename = f"{args.output_dir}/{timestamp}_{src}_{dst}.blob" + with open(output_filename, 'wb') as f: + f.write(binary_payload) + +if __name__ == '__main__': + main() diff --git a/py/poetry.lock b/py/poetry.lock new file mode 100644 index 0000000..e7828d5 --- /dev/null +++ b/py/poetry.lock @@ -0,0 +1,151 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "lxml" +version = "4.9.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, + {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, + {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, + {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, + {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, + {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, + {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, + {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, + {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, + {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, + {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, + {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, + {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, + {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, + {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, + {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, + {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, + {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, + {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, + {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, + {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pyshark" +version = "0.5.3" +description = "Python wrapper for tshark, allowing python packet parsing using wireshark dissectors" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pyshark-0.5.3-py3-none-any.whl", hash = "sha256:41bc2781066715a00198a00074bf659222822cbe74f36ea88663386aa7d9aa2b"}, + {file = "pyshark-0.5.3.tar.gz", hash = "sha256:41f015d803d648a1e655d7dba8a7635129dc0403bb887b3c9bdef1d6b09d9882"}, +] + +[package.dependencies] +appdirs = "*" +lxml = "*" +packaging = "*" +py = "*" + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "618f4c56b962b25095ca40e1c826b9dfd17249c3ca67509b23a80f209f8f205a" diff --git a/py/pyproject.toml b/py/pyproject.toml new file mode 100644 index 0000000..0dc1ffc --- /dev/null +++ b/py/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "feig-analysis" +version = "0.1.0" +description = "" +authors = ["Holger Rapp "] +readme = "README.md" +packages = [{include = "feig_analysis"}] + +[tool.poetry.dependencies] +python = "^3.9" +pyshark = "^0.5.3" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/zvt/BUILD.bazel b/zvt/BUILD.bazel new file mode 100644 index 0000000..db20264 --- /dev/null +++ b/zvt/BUILD.bazel @@ -0,0 +1,37 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@crate_index//:defs.bzl", "all_crate_deps") + +rust_library( + name = "zvt", + srcs = glob( + ["src/**/*.rs"], + exclude = ["src/main.rs"], + ), + crate_name = "zvt", + edition = "2021", + proc_macro_deps = all_crate_deps(proc_macro = True) + ["//zvt_derive"], + visibility = ["//visibility:public"], + deps = all_crate_deps() + ["//zvt_builder"], +) + +rust_binary( + name = "status", + srcs = glob(["examples/status.rs"]), + edition = "2021", + deps = all_crate_deps() + [":zvt"], +) + +rust_binary( + name = "feig_update", + srcs = glob(["examples/feig_update.rs"]), + edition = "2021", + deps = all_crate_deps() + [":zvt"], +) + +rust_test( + name = "zvt_test", + srcs = [], + crate = ":zvt", + data = glob(["data/*.blob"]), + edition = "2021", +) diff --git a/zvt/Cargo.toml b/zvt/Cargo.toml new file mode 100644 index 0000000..ba70920 --- /dev/null +++ b/zvt/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "zvt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.70" +chrono = "0.4.24" +clap = { version = "4.2.4", features = ["derive"] } +hex = "0.4.3" +yore = "1.0.2" +zvt_derive = { version = "0.1.0", path = "../zvt_derive" } +zvt_builder = { version = "0.1.0", path = "../zvt_builder" } +log = "0.4.19" +env_logger = "0.10.0" +tokio-stream = "0.1.14" +tokio = { version = "1.29.1", features = ["net", "io-util"] } +async-stream = "0.3.5" +serde = { version = "1.0.185", features = ["derive"] } +serde_json = "1.0.105" +futures = "0.3.28" + + +[dev-dependencies] +tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] } + +[[example]] +name = "feig_update" + +[[example]] +name = "status" diff --git a/zvt/data/1680722649.972316000_ecr_pt.blob b/zvt/data/1680722649.972316000_ecr_pt.blob new file mode 100644 index 0000000..70f3e4b --- /dev/null +++ b/zvt/data/1680722649.972316000_ecr_pt.blob @@ -0,0 +1 @@ +ÀüÐ` \ No newline at end of file diff --git a/zvt/data/1680728161.963129000_pt_ecr.blob b/zvt/data/1680728161.963129000_pt_ecr.blob new file mode 100644 index 0000000..6618459 Binary files /dev/null and b/zvt/data/1680728161.963129000_pt_ecr.blob differ diff --git a/zvt/data/1680728162.033575000_ecr_pt.blob b/zvt/data/1680728162.033575000_ecr_pt.blob new file mode 100644 index 0000000..8bb9db6 Binary files /dev/null and b/zvt/data/1680728162.033575000_ecr_pt.blob differ diff --git a/zvt/data/1680728162.647465000_pt_ecr.blob b/zvt/data/1680728162.647465000_pt_ecr.blob new file mode 100644 index 0000000..215fe30 --- /dev/null +++ b/zvt/data/1680728162.647465000_pt_ecr.blob @@ -0,0 +1 @@ +ÿ \ No newline at end of file diff --git a/zvt/data/1680728165.675509000_pt_ecr.blob b/zvt/data/1680728165.675509000_pt_ecr.blob new file mode 100644 index 0000000..c69082b Binary files /dev/null and b/zvt/data/1680728165.675509000_pt_ecr.blob differ diff --git a/zvt/data/1680728165.827009000_pt_ecr.blob b/zvt/data/1680728165.827009000_pt_ecr.blob new file mode 100644 index 0000000..3f52a8f Binary files /dev/null and b/zvt/data/1680728165.827009000_pt_ecr.blob differ diff --git a/zvt/data/1680728213.562478000_ecr_pt.blob b/zvt/data/1680728213.562478000_ecr_pt.blob new file mode 100644 index 0000000..7f432d1 --- /dev/null +++ b/zvt/data/1680728213.562478000_ecr_pt.blob @@ -0,0 +1 @@ +%@‡1I x \ No newline at end of file diff --git a/zvt/data/1680728215.585561000_pt_ecr.blob b/zvt/data/1680728215.585561000_pt_ecr.blob new file mode 100644 index 0000000..fd88c30 Binary files /dev/null and b/zvt/data/1680728215.585561000_pt_ecr.blob differ diff --git a/zvt/data/1680728215.659492000_pt_ecr.blob b/zvt/data/1680728215.659492000_pt_ecr.blob new file mode 100644 index 0000000..043332a Binary files /dev/null and b/zvt/data/1680728215.659492000_pt_ecr.blob differ diff --git a/zvt/data/1680728219.054216000_pt_ecr.blob b/zvt/data/1680728219.054216000_pt_ecr.blob new file mode 100644 index 0000000..6afb85e Binary files /dev/null and b/zvt/data/1680728219.054216000_pt_ecr.blob differ diff --git a/zvt/data/1680761818.641601000_pt_ecr.blob b/zvt/data/1680761818.641601000_pt_ecr.blob new file mode 100644 index 0000000..2df58eb --- /dev/null +++ b/zvt/data/1680761818.641601000_pt_ecr.blob @@ -0,0 +1,2 @@ + +)RR55I x \ No newline at end of file diff --git a/zvt/data/1680761818.690979000_ecr_pt.blob b/zvt/data/1680761818.690979000_ecr_pt.blob new file mode 100644 index 0000000..77b3e14 Binary files /dev/null and b/zvt/data/1680761818.690979000_ecr_pt.blob differ diff --git a/zvt/data/1680761818.768770000_pt_ecr.blob b/zvt/data/1680761818.768770000_pt_ecr.blob new file mode 100644 index 0000000..55b17b0 --- /dev/null +++ b/zvt/data/1680761818.768770000_pt_ecr.blob @@ -0,0 +1 @@ +%17FD1E3CGER-APP-v2.0.9 5252353524.4 \ No newline at end of file diff --git a/zvt/data/1680761828.489701000_pt_ecr.blob b/zvt/data/1680761828.489701000_pt_ecr.blob new file mode 100644 index 0000000..72871a8 Binary files /dev/null and b/zvt/data/1680761828.489701000_pt_ecr.blob differ diff --git a/zvt/data/1681273860.511128000_ecr_pt.blob b/zvt/data/1681273860.511128000_ecr_pt.blob new file mode 100644 index 0000000..2f70127 Binary files /dev/null and b/zvt/data/1681273860.511128000_ecr_pt.blob differ diff --git a/zvt/data/1681282621.302434000_ecr_pt.blob b/zvt/data/1681282621.302434000_ecr_pt.blob new file mode 100644 index 0000000..f86c581 --- /dev/null +++ b/zvt/data/1681282621.302434000_ecr_pt.blob @@ -0,0 +1 @@ +P4V \ No newline at end of file diff --git a/zvt/data/1681455683.221609000_ecr_pt.blob b/zvt/data/1681455683.221609000_ecr_pt.blob new file mode 100644 index 0000000..ac8d506 Binary files /dev/null and b/zvt/data/1681455683.221609000_ecr_pt.blob differ diff --git a/zvt/data/1682066249.409078000_pt_ecr.blob b/zvt/data/1682066249.409078000_pt_ecr.blob new file mode 100644 index 0000000..af12964 Binary files /dev/null and b/zvt/data/1682066249.409078000_pt_ecr.blob differ diff --git a/zvt/data/1682080275.594788000_192.168.0.139_192.168.0.59.blob b/zvt/data/1682080275.594788000_192.168.0.139_192.168.0.59.blob new file mode 100644 index 0000000..c3e16c2 Binary files /dev/null and b/zvt/data/1682080275.594788000_192.168.0.139_192.168.0.59.blob differ diff --git a/zvt/data/1682080275.777628000_192.168.0.59_192.168.0.139.blob b/zvt/data/1682080275.777628000_192.168.0.59_192.168.0.139.blob new file mode 100644 index 0000000..155f389 Binary files /dev/null and b/zvt/data/1682080275.777628000_192.168.0.59_192.168.0.139.blob differ diff --git a/zvt/data/1682080310.907262000_192.168.0.139_192.168.0.59.blob b/zvt/data/1682080310.907262000_192.168.0.139_192.168.0.59.blob new file mode 100644 index 0000000..143ea3c Binary files /dev/null and b/zvt/data/1682080310.907262000_192.168.0.139_192.168.0.59.blob differ diff --git a/zvt/data/partial_reversal.blob b/zvt/data/partial_reversal.blob new file mode 100644 index 0000000..89516f9 --- /dev/null +++ b/zvt/data/partial_reversal.blob @@ -0,0 +1 @@ +¸‡ÿÿ \ No newline at end of file diff --git a/zvt/data/print_system_configuration_reply.blob b/zvt/data/print_system_configuration_reply.blob new file mode 100644 index 0000000..95ce1f8 Binary files /dev/null and b/zvt/data/print_system_configuration_reply.blob differ diff --git a/zvt/examples/feig_update.rs b/zvt/examples/feig_update.rs new file mode 100644 index 0000000..aaf8e6d --- /dev/null +++ b/zvt/examples/feig_update.rs @@ -0,0 +1,152 @@ +use anyhow::Result; +use clap::Parser; +use serde::Deserialize; +use std::fs::read_to_string; +use std::path::PathBuf; +use tokio::net::TcpStream; +use tokio_stream::StreamExt; +use zvt::{feig, packets, sequences, sequences::Sequence}; + +/// Updates a feig terminal. +#[derive(Parser)] +struct Args { + /// The ip and port of the payment terminal. + #[clap(long, default_value = "localhost:22000")] + ip_address: String, + + /// The password of the payment terminal. The password is a 6-digits code, + /// e.x. 123456. + #[clap(long)] + password: usize, + + /// The config byte for the registration. + #[clap(long, default_value = "222")] + config_byte: u8, + + /// Force the update. The update will otherwise be skipped if the returned + /// software version corresponds to the version stored in app1/update.spec. + #[clap(long, default_value = "false")] + force: bool, + + /// The folder containing the payload, e.x. firmware and app1 folders. + payload_dir: PathBuf, +} + +#[derive(Deserialize)] +struct UpdateSpec { + version: String, +} + +/// Returns the desired version of the App. +/// +/// We're using the app1/update.spec as a proxy for the version of the entire +/// firmware update. Returns an error if the desired version cannot be read. +fn get_desired_version(payload_dir: &std::path::PathBuf) -> Result { + let path = payload_dir.join("app1/update.spec"); + let update_spec_str = read_to_string(&path)?; + let update_spec: UpdateSpec = serde_json::from_str(&update_spec_str)?; + Ok(update_spec.version) +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + // Connect to the payment terminal. + let mut socket = TcpStream::connect(&args.ip_address).await?; + const MAX_LEN_ADPU: u16 = 1u16 << 15; + let registration = packets::Registration { + password: args.password, + config_byte: args.config_byte, + currency: None, + tlv: Some(packets::tlv::Registration { + max_len_adpu: Some(MAX_LEN_ADPU), + }), + }; + + { + // Register to the terminal. + let mut stream = sequences::Registration::into_stream(®istration, &mut socket); + while let Some(response) = stream.next().await { + match response { + Err(_) => panic!("Failed to register to the terminal"), + Ok(completion) => println!("Registered to the terminal {:?}", completion), + } + } + } + + { + // Check the current version of the software + let request = feig::packets::CVendFunctions { instr: 1 }; + let mut stream = feig::sequences::GetSystemInfo::into_stream(&request, &mut socket); + let mut current_version = "unknown".to_string(); + while let Some(response) = stream.next().await { + match response { + Err(_) => panic!("Failed to get the system info"), + Ok(completion) => { + println!("The system info returned {:?}", completion); + if let feig::sequences::GetSystemInfoResponse::CVendFunctionsEnhancedSystemInformationCompletion(packet) = completion { + current_version = packet.sw_version; + } + } + } + } + + // Check if we have to run the update. + if !args.force { + match get_desired_version(&args.payload_dir) { + Ok(desired_version) => { + // We can't go for strict equality since the desired version + // contains just a semantic version e.x. `2.0.12` and the + // actual also contains the language e.x. `GER-APP-v2.0.12`. + if current_version.contains(&desired_version) { + println!("Skipping update"); + return Ok(()); + } + } + Err(err) => println!("Failed to get the current version {}", err), + } + } + } + + // If the terminal has a pending EOD job the update will fail. Therefore + // we precautionary run the EOD job here. However, if the payment terminal + // is not setup yet, the EOD will fail. We therefore ignore all errors + // during the EOD job. + { + let request = packets::EndOfDay { + password: args.password, + }; + let mut stream = sequences::EndOfDay::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + println!("The EndOfDay returned {response:?}"); + } + } + + { + // Update the app. + let mut stream = feig::sequences::WriteFile::into_stream( + args.payload_dir, + args.password, + MAX_LEN_ADPU.into(), + &mut socket, + ); + while let Some(response) = stream.next().await { + match response { + Err(_) => panic!("Failed to update the terminal"), + Ok(inner) => { + println!("Updating the terminal {:?}", inner); + match inner { + feig::sequences::WriteFileResponse::Abort(abort) => { + panic!("Failed to update the terminal {abort:?}") + } + _ => {} + } + } + } + } + println!("Finished the update"); + } + + Ok(()) +} diff --git a/zvt/examples/status.rs b/zvt/examples/status.rs new file mode 100644 index 0000000..ee8bff7 --- /dev/null +++ b/zvt/examples/status.rs @@ -0,0 +1,157 @@ +use clap::Parser; +use env_logger::{Builder, Env}; +use log::info; +use std::io::Write; +use tokio::net::TcpStream; +use tokio_stream::StreamExt; +use zvt::{packets, sequences, sequences::Sequence}; + +#[derive(Parser, Debug)] +struct Args { + /// The ip and port of the payment terminal. + #[clap(long, default_value = "localhost:22000")] + ip: String, + + /// The password of the payment terminal. + #[clap(long, default_value = "123456")] + password: usize, + + /// The config byte for the registration. Defaults to 0xDE (= 222). + #[clap(long, default_value = "222")] + config_byte: u8, + + /// The currency code + #[clap(long, default_value = "978")] + currency_code: usize, + + /// The terminal id to be set. + #[clap(long, default_value = "123456")] + terminal_id: usize, +} + +fn init_logger() { + let env = Env::default().filter_or("ZVT_LOGGER_LEVEL", "info"); + + Builder::from_env(env) + .format(|buf, record| { + writeln!( + buf, + "<{}>{}: {}", + match record.level() { + log::Level::Error => 3, + log::Level::Warn => 4, + log::Level::Info => 6, + log::Level::Debug => 7, + log::Level::Trace => 7, + }, + record.target(), + record.args() + ) + }) + .init(); +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + init_logger(); + let args = Args::parse(); + + info!("Using the args {:?}", args); + let mut socket = TcpStream::connect(args.ip).await?; + + let request = packets::Registration { + password: args.password, + config_byte: args.config_byte, + currency: Some(args.currency_code), + tlv: None, + }; + + info!("Running Registration with {request:?}"); + + let mut stream = sequences::Registration::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to Registration: {:?}", response); + } + drop(stream); + + let request = packets::SetTerminalId { + password: args.password, + terminal_id: Some(args.terminal_id), + }; + info!("Running SetTerminalId with {request:?}"); + let mut stream = sequences::SetTerminalId::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to SetTerminalId: {:?}", response); + } + drop(stream); + + let request = packets::Initialization { + password: args.password, + }; + info!("Running Initialization with {request:?}"); + let mut stream = sequences::Initialization::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to Initialization: {:?}", response); + } + drop(stream); + + let request = packets::Diagnosis { + tlv: Some(packets::tlv::Diagnosis { + diagnosis_type: Some(1), + }), + }; + info!("Running Diagnosing with {request:?}"); + let mut stream = sequences::Diagnosis::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to Diagnosis: {:?}", response); + } + drop(stream); + + let request = packets::PrintSystemConfiguration {}; + info!("Running PrintSystemConfiguration"); + let mut stream = sequences::PrintSystemConfiguration::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to PrintSystemConfiguration: {:?}", response); + } + drop(stream); + + let request = packets::EndOfDay { + password: args.password, + }; + + info!("Running EndOfDay with {request:?}"); + let mut stream = sequences::EndOfDay::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to EndOfDay: {:?}", response); + } + drop(stream); + + let request = packets::StatusEnquiry { + password: None, + service_byte: None, + tlv: None, + }; + info!("Running StatusEnquiry with {request:?}"); + let mut stream = sequences::StatusEnquiry::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to StatusEnquiry: {:?}", response); + } + drop(stream); + + let request = packets::PartialReversal { + receipt_no: Some(0xffff), + amount: None, + payment_type: None, + currency: None, + tlv: None, + }; + + info!("Running PartialReversalData with {request:?}"); + let mut stream = sequences::PartialReversal::into_stream(&request, &mut socket); + while let Some(response) = stream.next().await { + info!("Response to PartialReversalData: {:?}", response); + } + drop(stream); + + Ok(()) +} diff --git a/zvt/src/constants.rs b/zvt/src/constants.rs new file mode 100644 index 0000000..65768d8 --- /dev/null +++ b/zvt/src/constants.rs @@ -0,0 +1,169 @@ +/// Messages as defined under chapter 10. +#[repr(u8)] +pub enum ErrorMessages { + CardNotReadable = 0x64, + CardDataNotPresent = 0x65, + ProcessingError = 0x66, + FunctionNotPermittedForEcAndMaestroCards = 0x67, + FunctionNotPermittedForCreditAndTankCards = 0x68, + TurnoverFileFull = 0x6a, + FunctionDeactivated = 0x6b, + AbortViaTimeoutOrAbortKey = 0x6c, + CardInBlockedList = 0x6e, + WrongCurrency = 0x6f, + CreditNotSufficient = 0x71, + ChipError = 0x72, + CardDataIncorrect = 0x73, + DukptEngineExhausted = 0x74, + TextNotAuthentic = 0x75, + PanNotInWhiteList = 0x76, + EndOfDayBatchNotPossible = 0x77, + CardExpired = 0x78, + CardNotYetValid = 0x79, + CardUnknown = 0x7a, + FallbackToMagneticStripeNotPossibleForGiroCard1 = 0x7b, + FallbackToMagneticStripeNotPossibleForNonGiroCard = 0x7c, + CommunicationError = 0x7d, + FallbackToMagneticStripeNotPossibleForGiroCard2 = 0x7e, + FunctionNotPossible = 0x83, + KeyMissing = 0x85, + PinPadDefective1 = 0x89, + ZvtProtocolError = 0x9a, + ErrorFromDialUp = 0x9b, + PleaseWait = 0x9c, + ReceiverNotReady = 0xa0, + RemoteStationDoesNotRespond = 0xa1, + NoConnection = 0xa3, + SubmissionOfGeldkarteNotPossible = 0xa4, + FunctionNotAllowedDueToPciDss = 0xa5, + MemoryFull = 0xb1, + MerchantJournalFull = 0xb2, + AlreadyReversed = 0xb4, + ReversalNotPossible = 0xb5, + PreAuthorizationIncorrect = 0xb7, + ErrorPreAuthorization = 0xb8, + VoltageSupplyToLow = 0xbf, + CardLockingMechanismDefective = 0xc0, + MerchantCardLocked = 0xc1, + DiagnosisRequired = 0xc2, + MaximumAmountExceeded = 0xc3, + CardProfileInvalid = 0xc4, + PaymentMethodNotSupported = 0xc5, + CurrencyNotApplicable = 0xc6, + AmountTooSmall = 0xc8, + MaxTransactionAmountTooSmall = 0xc9, + FunctionOnlyAllowedInEuro = 0xcb, + PrinterNotReady = 0xcc, + CashbackNotPossible = 0xcd, + FunctionNotPermittedForServiceCards = 0xd2, + CardInserted = 0xdc, + ErrorDuringCardEject = 0xdd, + ErrorDuringCardInsertion = 0xde, + RemoteMaintenanceActivated = 0xe0, + CardReaderDoesNotAnswer = 0xe2, + ShutterClosed = 0xe3, + TerminalActivationRequired = 0xe4, + MinOneGoodsGroupNotFound = 0xe7, + NoGoodsGroupsTableLoaded = 0xe8, + RestrictionCodeNotPermitted = 0xe9, + CardCodeNotPermitted = 0xea, + FunctionNotExecutable = 0xeb, + PinProcessingNotPossible = 0xec, + PinPadDefective2 = 0xed, + OpenEndOfDayBatchPresent = 0xf0, + EcCashOrMaestroOfflineError = 0xf1, + OptError = 0xf5, + OptDataNotAvailable = 0xf6, + ErrorTransmittingOfflineTransactions = 0xfa, + TurnoverDataSetDefective = 0xfb, + NecessaryDeviceNotPresentOrDefective = 0xfc, + BaudRateNotSupported = 0xfd, + RegisterUnknown = 0xfe, + SystemError = 0xff, +} + +impl std::fmt::Display for ErrorMessages { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CardNotReadable => write!(f, "card not readable (LRC-/parity-error)"), + Self::CardDataNotPresent => write!(f, "card-data not present (neither track-data nor chip found)"), + Self::ProcessingError => write!(f, "processing-error (also for problems with card-reader mechanism)"), + Self::FunctionNotPermittedForEcAndMaestroCards => write!(f, "function not permitted for ec- and Maestro-cards"), + Self::FunctionNotPermittedForCreditAndTankCards => write!(f, "function not permitted for credit- and tank-cards"), + Self::TurnoverFileFull => write!(f, "turnover-file full"), + Self::FunctionDeactivated => write!(f, "function deactivated (PT not registered)"), + Self::AbortViaTimeoutOrAbortKey => write!(f, "abort via timeout or abort-key"), + Self::CardInBlockedList => write!(f, "card in blocked-list (response to command 06 E4)"), + Self::WrongCurrency => write!(f, "wrong currency"), + Self::CreditNotSufficient => write!(f, "credit not sufficient (chip-card)"), + Self::ChipError => write!(f, "chip error"), + Self::CardDataIncorrect => write!(f, "card-data incorrect (e.g. country-key check, checksum-error)"), + Self::DukptEngineExhausted => write!(f, "DUKPT engine exhausted"), + Self::TextNotAuthentic => write!(f, "text not authentic"), + Self::PanNotInWhiteList => write!(f, "PAN not in white list"), + Self::EndOfDayBatchNotPossible => write!(f, "end-of-day batch not possible"), + Self::CardExpired => write!(f, "card expired"), + Self::CardNotYetValid => write!(f, "card not yet valid"), + Self::CardUnknown => write!(f, "card unknown"), + Self::FallbackToMagneticStripeNotPossibleForGiroCard1 => write!(f, "fallback to magnetic stripe for girocard not possible"), + Self::FallbackToMagneticStripeNotPossibleForNonGiroCard => write!(f, "fallback to magnetic stripe not possible (used for non girocard cards)"), + Self::CommunicationError => write!(f, "communication error (communication module does not answer or is not present)"), + Self::FallbackToMagneticStripeNotPossibleForGiroCard2 => write!(f, "fallback to magnetic stripe not possible, debit advice possible (used only for giro-card)"), + Self::FunctionNotPossible => write!(f, "function not possible"), + Self::KeyMissing => write!(f, "key missing"), + Self::PinPadDefective1 => write!(f, "PIN-pad defective"), + Self::ZvtProtocolError => write!(f, "ZVT protocol error. e. g. parsing error, mandatory message element missing"), + Self::ErrorFromDialUp => write!(f, "error from dial-up/communication fault"), + Self::PleaseWait => write!(f, "please wait"), + Self::ReceiverNotReady => write!(f, "receiver not ready"), + Self::RemoteStationDoesNotRespond => write!(f, "remote station does not respond"), + Self::NoConnection => write!(f, "no connection"), + Self::SubmissionOfGeldkarteNotPossible => write!(f, "submission of Geldkarte not possible"), + Self::FunctionNotAllowedDueToPciDss => write!(f, "function not allowed due to PCI-DSS/P2PE rules"), + Self::MemoryFull => write!(f, "memory full"), + Self::MerchantJournalFull => write!(f, "merchant-journal full"), + Self::AlreadyReversed => write!(f, "already reversed"), + Self::ReversalNotPossible => write!(f, "reversal not possible"), + Self::PreAuthorizationIncorrect => write!(f, "pre-authorization incorrect (amount too high) or amount wrong"), + Self::ErrorPreAuthorization => write!(f, "error pre-authorization"), + Self::VoltageSupplyToLow => write!(f, "voltage supply to low (external power supply)"), + Self::CardLockingMechanismDefective => write!(f, "card locking mechanism defective"), + Self::MerchantCardLocked => write!(f, "merchant-card locked"), + Self::DiagnosisRequired => write!(f, "diagnosis required"), + Self::MaximumAmountExceeded => write!(f, "maximum amount exceeded"), + Self::CardProfileInvalid => write!(f, "card-profile invalid. New card-profiles must be loaded."), + Self::PaymentMethodNotSupported => write!(f, "payment method not supported"), + Self::CurrencyNotApplicable => write!(f, "currency not applicable"), + Self::AmountTooSmall => write!(f, "amount too small"), + Self::MaxTransactionAmountTooSmall => write!(f, "max. transaction-amount too small"), + Self::FunctionOnlyAllowedInEuro => write!(f, "function only allowed in EURO"), + Self::PrinterNotReady => write!(f, "printer not ready"), + Self::CashbackNotPossible => write!(f, "Cashback not possible"), + Self::FunctionNotPermittedForServiceCards => write!(f, "function not permitted for service-cards/bank-customer-cards"), + Self::CardInserted => write!(f, "card inserted"), + Self::ErrorDuringCardEject => write!(f, "error during card-eject (for motor-insertion reader)"), + Self::ErrorDuringCardInsertion => write!(f, "error during card-insertion (for motor-insertion reader)"), + Self::RemoteMaintenanceActivated => write!(f, "remote-maintenance activated"), + Self::CardReaderDoesNotAnswer => write!(f, "card-reader does not answer / card-reader defective"), + Self::ShutterClosed => write!(f, "shutter closed"), + Self::TerminalActivationRequired => write!(f, "Terminal activation required"), + Self::MinOneGoodsGroupNotFound => write!(f, "min. one goods-group not found"), + Self::NoGoodsGroupsTableLoaded => write!(f, "no goods-groups-table loaded"), + Self::RestrictionCodeNotPermitted => write!(f, "restriction-code not permitted"), + Self::CardCodeNotPermitted => write!(f, "card-code not permitted (e.g. card not activated via Diagnosis)"), + Self::FunctionNotExecutable => write!(f, "function not executable (PIN-algorithm unknown)"), + Self::PinProcessingNotPossible => write!(f, "PIN-processing not possible"), + Self::PinPadDefective2 => write!(f, "PIN-pad defective"), + Self::OpenEndOfDayBatchPresent => write!(f, "open end-of-day batch present"), + Self::EcCashOrMaestroOfflineError => write!(f, "ec-cash/Maestro offline error"), + Self::OptError => write!(f, "OPT-error"), + Self::OptDataNotAvailable => write!(f, "OPT-data not available (= OPT personalization required)"), + Self::ErrorTransmittingOfflineTransactions => write!(f, "error transmitting offline-transactions (clearing error)"), + Self::TurnoverDataSetDefective => write!(f, "turnover data-set defective"), + Self::NecessaryDeviceNotPresentOrDefective => write!(f, "necessary device not present or defective"), + Self::BaudRateNotSupported => write!(f, "baudrate not supported"), + Self::RegisterUnknown => write!(f, "register unknown"), + Self::SystemError => write!(f, "system error (= other/unknown error), See TLV tags 1F16 and 1F17"), + } + } +} diff --git a/zvt/src/feig/mod.rs b/zvt/src/feig/mod.rs new file mode 100644 index 0000000..0735102 --- /dev/null +++ b/zvt/src/feig/mod.rs @@ -0,0 +1,2 @@ +pub mod packets; +pub mod sequences; diff --git a/zvt/src/feig/packets/mod.rs b/zvt/src/feig/packets/mod.rs new file mode 100644 index 0000000..2575556 --- /dev/null +++ b/zvt/src/feig/packets/mod.rs @@ -0,0 +1,225 @@ +pub mod tlv; +use crate::{encoding, length, ZVTError, ZVTResult, Zvt}; + +/// From feig, 6.13 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x04, instr = 0x0c)] +pub struct RequestForData { + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// Custom length parsing for temperatures. +/// +/// The specification says that the temperature has always the length of four +/// bytes. However, we observed that for low temperatures the terminals return +/// only three bytes. We implement a custom decoder which decodes both lengths. +/// This can be removed once the bug is fixed. See +/// https://groups.google.com/a/qwello.eu/g/embedded-team/c/Ma5XyYzkdTQ/m/ZrSLp1tVAAAJ +/// for more details. +struct Temperature {} + +impl length::Length for Temperature { + fn deserialize(bytes: &[u8]) -> ZVTResult<(usize, &[u8])> { + if bytes.len() < 3 { + return Err(ZVTError::IncompleteData); + } + let len = std::cmp::min(bytes.len(), 4); + Ok((len, bytes)) + } + + fn serialize(_len: usize) -> Vec { + vec![] + } +} + +/// From Feig manual, 6.3 Enhanced system information. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x0f)] +pub struct CVendFunctionsEnhancedSystemInformationCompletion { + #[zvt_bmp(length = length::Fixed<8>)] + pub device_id: String, + + #[zvt_bmp(length = length::Fixed<17>)] + pub sw_version: String, + + #[zvt_bmp(length = length::Fixed<8>)] + pub terminal_id: String, + + #[zvt_bmp(length = Temperature)] + pub temperature: String, +} + +/// From Feig specific manual, 6.13 - Write File. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x08, instr = 0x14)] +pub struct WriteFile { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: usize, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// Feig, 5.1 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x0f, instr = 0xa1)] +pub struct CVendFunctions { + #[zvt_bmp( encoding = encoding::BigEndian)] + pub instr: u16, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x80, instr = 0x00)] +pub struct WriteData { + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::packets::tests::get_bytes; + use crate::ZvtSerializer; + + #[test] + fn test_request_for_data() { + let bytes = get_bytes("1682080275.777628000_192.168.0.59_192.168.0.139.blob"); + let expected = RequestForData { + tlv: Some(tlv::WriteData { + file: Some(tlv::File { + file_id: Some(0x23), + file_offset: Some(65000), + file_size: None, + payload: None, + }), + }), + }; + assert_eq!(RequestForData::zvt_deserialize(&bytes).unwrap().0, expected); + assert_eq!(bytes, expected.zvt_serialize()); + } + + #[test] + fn test_cv_end_functions_enhanced_systems_information_completion() { + let bytes = get_bytes("1680761818.768770000_pt_ecr.blob"); + let expected = CVendFunctionsEnhancedSystemInformationCompletion { + device_id: "17FD1E3C".to_string(), + sw_version: "GER-APP-v2.0.9 ".to_string(), + terminal_id: "52523535".to_string(), + temperature: "24.4".to_string(), + }; + assert_eq!( + CVendFunctionsEnhancedSystemInformationCompletion::zvt_deserialize(&bytes) + .unwrap() + .0, + expected + ); + assert_eq!(bytes, expected.zvt_serialize()); + + // Test the temperature encoding bug. + let bytes = b"\x06\x0f$17FE5C90GER-APP-v2.0.9 525251118.0"; + let expected = CVendFunctionsEnhancedSystemInformationCompletion { + device_id: "17FE5C90".to_string(), + sw_version: "GER-APP-v2.0.9 ".to_string(), + terminal_id: "52525111".to_string(), + temperature: "8.0".to_string(), + }; + + assert_eq!( + CVendFunctionsEnhancedSystemInformationCompletion::zvt_deserialize(bytes) + .unwrap() + .0, + expected + ); + } + + #[test] + fn test_write_file() { + let bytes = get_bytes("1682080275.594788000_192.168.0.139_192.168.0.59.blob"); + let expected = WriteFile { + password: 123456, + tlv: Some(tlv::WriteFile { + files: vec![ + tlv::File { + file_id: Some(0x23), + file_size: Some(3357255), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x22), + file_size: Some(3611), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x12), + file_size: Some(125825), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x10), + file_size: Some(3479044), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x11), + file_size: Some(10909539), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x13), + file_size: Some(1068), + file_offset: None, + payload: None, + }, + tlv::File { + file_id: Some(0x14), + file_size: Some(1160), + file_offset: None, + payload: None, + }, + ], + }), + }; + assert_eq!(WriteFile::zvt_deserialize(&bytes).unwrap().0, expected); + assert_eq!(bytes, expected.zvt_serialize()); + } + + #[test] + fn test_cvend_functions() { + let bytes = get_bytes("1680761818.690979000_ecr_pt.blob"); + let expected = CVendFunctions { instr: 0x01 }; + assert_eq!(CVendFunctions::zvt_deserialize(&bytes).unwrap().0, expected); + assert_eq!(bytes, expected.zvt_serialize()); + } + + #[test] + fn test_write_data() { + let bytes = get_bytes("1682080310.907262000_192.168.0.139_192.168.0.59.blob"); + let dummy_data = vec![0; 0x042c]; + let expected = WriteData { + tlv: Some(tlv::WriteData { + file: Some(tlv::File { + file_id: Some(0x13), + file_offset: Some(0), + file_size: None, + payload: Some(dummy_data.clone()), // dummy data. + }), + }), + }; + let mut actual = WriteData::zvt_deserialize(&bytes).unwrap().0; + let file = actual.tlv.as_mut().unwrap().file.as_mut().unwrap(); + assert_eq!(file.payload.as_ref().unwrap().len(), dummy_data.len()); + // Replace the data with the dummy data. + file.payload = Some(dummy_data); + assert_eq!(actual, expected); + + // Serialize back to bytes and compare everything up to the payload. + let actual_bytes = expected.zvt_serialize(); + assert_eq!(actual_bytes[..26], bytes[..26]); + } +} diff --git a/zvt/src/feig/packets/tlv.rs b/zvt/src/feig/packets/tlv.rs new file mode 100644 index 0000000..59fb450 --- /dev/null +++ b/zvt/src/feig/packets/tlv.rs @@ -0,0 +1,78 @@ +use crate::{ + encoding, encoding::Encoding, length, length::Length, Tag, ZVTError, ZVTResult, Zvt, + ZvtSerializerImpl, +}; + +/// Custom encoder for a place vector string. +pub struct Custom; + +impl encoding::Encoding> for Custom { + fn encode(input: &Vec) -> Vec { + input.clone() + } + + fn decode(bytes: &[u8]) -> ZVTResult<(Vec, &[u8])> { + Ok((bytes.to_vec(), &[])) + } +} + +/// Custom [ZvtSerializerImpl] which will just copy the data. +impl> ZvtSerializerImpl for Vec { + fn deserialize_tagged(mut bytes: &[u8], tag: Option) -> ZVTResult<(Self, &[u8])> { + if let Some(desired_tag) = tag { + let actual_tag; + (actual_tag, bytes) = TE::decode(bytes)?; + if actual_tag != desired_tag { + return Err(ZVTError::WrongTag(actual_tag)); + } + } + let (length, payload) = length::Tlv::deserialize(bytes)?; + if length > payload.len() { + return Err(ZVTError::IncompleteData); + } + let (data, remainder) = Custom::decode(&payload[..length])?; + + Ok((data, &payload[length - remainder.len()..])) + } + + fn serialize_tagged(&self, tag: Option) -> Vec { + let mut output = Vec::new(); + if self.is_empty() { + return output; + } + if let Some(tag) = tag { + output = TE::encode(&tag); + } + let mut length = length::Tlv::serialize(self.len()); + output.append(&mut length); + output.append(&mut Custom::encode(self)); + output + } +} + +#[derive(Debug, PartialEq, Zvt, Default)] +pub struct File { + #[zvt_tlv(tag = 0x1d)] + pub file_id: Option, + + #[zvt_tlv(tag = 0x1e, encoding = encoding::BigEndian)] + pub file_offset: Option, + + #[zvt_tlv(tag = 0x1f00, encoding = encoding::BigEndian)] + pub file_size: Option, + + #[zvt_tlv(tag = 0x1c, encoding = Custom)] + pub payload: Option>, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct WriteData { + #[zvt_tlv(tag = 0x2d)] + pub file: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct WriteFile { + #[zvt_tlv(tag = 0x2d)] + pub files: Vec, +} diff --git a/zvt/src/feig/sequences.rs b/zvt/src/feig/sequences.rs new file mode 100644 index 0000000..232dd19 --- /dev/null +++ b/zvt/src/feig/sequences.rs @@ -0,0 +1,207 @@ +use crate::sequences::{read_packet_async, write_with_ack_async, Sequence}; +use crate::{packets, ZvtEnum, ZvtParser, ZvtSerializer}; +use anyhow::Result; +use async_stream::try_stream; +use std::boxed::Box; +use std::collections::HashMap; +use std::io::Seek; +use std::io::{Error, ErrorKind}; +use std::marker::Unpin; +use std::os::unix::fs::FileExt; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_stream::Stream; +use zvt_builder::ZVTError; + +pub struct GetSystemInfo; + +#[derive(Debug, ZvtEnum)] +pub enum GetSystemInfoResponse { + CVendFunctionsEnhancedSystemInformationCompletion( + super::packets::CVendFunctionsEnhancedSystemInformationCompletion, + ), + Abort(packets::Abort), +} + +impl Sequence for GetSystemInfo { + type Input = super::packets::CVendFunctions; + type Output = GetSystemInfoResponse; +} + +pub struct WriteFile; + +pub struct File { + /// The id as in 6.13, Table 2. + pub file_id: u8, + + /// The file path. + pub path: String, +} + +fn convert_dir(dir: &Path) -> Result> { + let valid_paths = [ + (Path::new("firmware/kernel.gz"), 0x10), + (Path::new("firmware/rootfs.gz"), 0x11), + (Path::new("firmware/components.tar.gz"), 0x12), + (Path::new("firmware/update.spec"), 0x13), + (Path::new("firmware/update_extended.spec"), 0x14), + (Path::new("app0/update.spec"), 0x20), + (Path::new("app0/update.tar.gz"), 0x21), + (Path::new("app1/update.spec"), 0x22), + (Path::new("app1/update.tar.gz"), 0x23), + (Path::new("app2/update.spec"), 0x24), + (Path::new("app2/update.tar.gz"), 0x25), + (Path::new("app3/update.spec"), 0x26), + (Path::new("app3/update.tar.gz"), 0x27), + (Path::new("app4/update.spec"), 0x28), + (Path::new("app4/update.tar.gz"), 0x29), + (Path::new("app5/update.spec"), 0x30), + (Path::new("app5/update.tar.gz"), 0x31), + (Path::new("app6/update.spec"), 0x32), + (Path::new("app6/update.tar.gz"), 0x33), + (Path::new("app7/update.spec"), 0x34), + (Path::new("app7/update.tar.gz"), 0x35), + ]; + let mut out = HashMap::new(); + + for (p, i) in valid_paths.iter() { + let full_path = dir.join(p); + if full_path.exists() { + out.insert(*i, full_path.into_os_string().into_string().unwrap()); + } + } + + if out.is_empty() { + return Err(Error::new( + ErrorKind::InvalidData, + "The directory contained no valid data", + ) + .into()); + } + Ok(out) +} + +#[derive(Debug, ZvtEnum)] +pub enum WriteFileResponse { + CompletionData(packets::CompletionData), + RequestForData(super::packets::RequestForData), + Abort(packets::Abort), +} + +impl WriteFile { + pub fn into_stream( + path: PathBuf, + password: usize, + adpu_size: u32, + src: &mut Source, + ) -> Pin> + '_>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin, + { + // Protocol from the handbook (the numbering is not part of the handbook) + // 1.1 ECR->PT: Send over the list of all files with their sizes. + // 1.2 PT->ECR: Ack + // 2.1 PT->ERC: Send a Request with file id and offset + // 2.2.ERC->PT: Send over the file + // The steps 2.1 and 2.2. may be repeated + // 3.0 PT->ERC replies with Completion. + + let s = try_stream! { + tokio::pin!(src); + + use super::packets::tlv::File as TlvFile; + let files = convert_dir(&path)?; + let mut packets = Vec::with_capacity(files.len()); + for f in files.iter() { + // Get the size. + let size = std::fs::File::open(f.1)?.seek(std::io::SeekFrom::End(0))?; + println!("The file {} has the size {}", f.1, size); + + // Convert to packet. + packets.push(TlvFile { + file_id: Some(*f.0), + file_size: Some(size as u32), + file_offset: None, + payload: None, + }); + } + + let packet = super::packets::WriteFile { + password, + tlv: Some(super::packets::tlv::WriteFile { files: packets }), + }; + + // 1.1. and 1.2 + write_with_ack_async(&packet, &mut src).await?; + let mut buf = vec![0; adpu_size as usize]; + println!("the length is {}", buf.len()); + + loop { + // Get the data. + let bytes = read_packet_async(&mut src).await?; + println!("The packet is {:?}", bytes); + + let response = WriteFileResponse::zvt_parse(&bytes)?; + + match response { + WriteFileResponse::CompletionData(_) => { + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + + yield response; + break; + } + WriteFileResponse::Abort(_) => { + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + + yield response; + break; + } + WriteFileResponse::RequestForData(ref data) => { + // Unwrap the request. + let request = data + .tlv + .as_ref() + .ok_or(ZVTError::IncompleteData)? + .file + .as_ref() + .ok_or(ZVTError::IncompleteData)?; + + let file_id = request.file_id.as_ref().ok_or(ZVTError::IncompleteData)?; + let file_offset = request + .file_offset + .as_ref() + .ok_or(ZVTError::IncompleteData)?; + + // Get the file from the hashmap + let file_path = files.get(file_id).ok_or(ZVTError::IncompleteData)?; + + // Read into the buffer. + let file = std::fs::File::open(file_path)?; + let read_bytes = file.read_at(&mut buf, *file_offset as u64)?; + + println!( + "Sending file {} at position {} with length {}", + file_path, file_offset, read_bytes + ); + + let packet = super::packets::WriteData { + tlv: Some(super::packets::tlv::WriteData { + file: Some(super::packets::tlv::File { + file_id: request.file_id, + file_offset: request.file_offset, + file_size: None, + payload: Some(buf[..read_bytes].to_vec()), + }), + }), + }; + src.write_all(&packet.zvt_serialize()).await?; + + yield response; + } + } + } + }; + Box::pin(s) + } +} diff --git a/zvt/src/lib.rs b/zvt/src/lib.rs new file mode 100644 index 0000000..6966a0a --- /dev/null +++ b/zvt/src/lib.rs @@ -0,0 +1,8 @@ +pub mod constants; +pub mod feig; +pub mod packets; +pub mod sequences; + +// Reexport everything so we can just use this crate for importing the internals. +pub use zvt_builder::*; +pub use zvt_derive::*; diff --git a/zvt/src/packets.rs b/zvt/src/packets.rs new file mode 100644 index 0000000..65f4831 --- /dev/null +++ b/zvt/src/packets.rs @@ -0,0 +1,760 @@ +use crate::{encoding, length, Zvt}; + +pub mod tlv; + +/// Implements the SetTimeAndDate packet. See chapter 3.4 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x04, instr = 0x01)] +pub struct SetTimeAndDate { + #[zvt_bmp(number = 0xaa, length = length::Fixed<3>, encoding = encoding::Bcd)] + pub date: usize, + + #[zvt_bmp(number = 0x0c, length = length::Fixed<3>, encoding = encoding::Bcd)] + pub time: usize, +} + +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct NumAndTotal { + pub num: u8, + + #[zvt_bmp(length = length::Fixed<6>, encoding = encoding::Bcd)] + pub total: usize, +} + +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct SingleAmounts { + #[zvt_bmp(length = length::Fixed<2>, encoding = encoding::Bcd)] + pub receipt_no_start: usize, + + #[zvt_bmp(length = length::Fixed<2>, encoding = encoding::Bcd)] + pub receipt_no_end: usize, + + pub girocard: NumAndTotal, + pub jcb: NumAndTotal, + pub eurocard: NumAndTotal, + pub amex: NumAndTotal, + pub visa: NumAndTotal, + pub diners: NumAndTotal, + pub others: NumAndTotal, +} + +#[derive(Debug, Default, PartialEq, Zvt)] +#[zvt_control_field(class = 0x04, instr = 0x0f)] +pub struct StatusInformation { + #[zvt_bmp(number = 0x04, length = length::Fixed<6>, encoding = encoding::Bcd)] + pub amount: Option, + + #[zvt_bmp(number = 0x0b, length = length::Fixed<3>, encoding = encoding::Bcd)] + pub trace_number: Option, + + #[zvt_bmp(number = 0x0c, length = length::Fixed<3>, encoding = encoding::Bcd)] + pub time: Option, + + #[zvt_bmp(number = 0x0d, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub date: Option, + + #[zvt_bmp(number = 0x0e, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub expiry_date: Option, + + #[zvt_bmp(number = 0x17, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub card_sequence_number: Option, + + #[zvt_bmp(number = 0x19)] + pub card_type: Option, + + #[zvt_bmp(number = 0x22, length = length::Llv, encoding = encoding::Bcd)] + pub card_number: Option, + + #[zvt_bmp(number = 0x27)] + pub result_code: Option, + + #[zvt_bmp(number = 0x29, length = length::Fixed<4>, encoding = encoding::Bcd)] + pub terminal_id: Option, + + #[zvt_bmp(number = 0x2a, length = length::Fixed<15>)] + pub vu_number: Option, + + #[zvt_bmp(number = 0x3b, length = length::Fixed<8>)] + pub aid_authorization_attribute: Option, + + #[zvt_bmp(number = 0x3c, length = length::Lllv)] + pub additional_text: Option, + + #[zvt_bmp(number = 0x60, length = length::Lllv)] + pub single_amounts: Option, + + #[zvt_bmp(number = 0x87, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub receipt_no: Option, + + #[zvt_bmp(number = 0x49, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x8a)] + pub zvt_card_type: Option, + + #[zvt_bmp(number = 0x8b, length = length::Llv)] + pub card_name: Option, + + #[zvt_bmp(number = 0x8c)] + pub zvt_card_type_id: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x04, instr = 0xff)] +pub struct IntermediateStatusInformation { + pub status: u8, + + #[zvt_bmp(encoding = encoding::Bcd)] + pub timeout: Option, +} + +/// Chapter 2.55 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x05, instr = 0x01)] +pub struct StatusEnquiry { + // TODO(ddo) the password must only be set if service byte is also set. + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: Option, + + #[zvt_bmp(number = 0x03)] + pub service_byte: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x00)] +pub struct Registration { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: usize, + + pub config_byte: u8, + + #[zvt_bmp(length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[derive(Debug, Default, PartialEq, Eq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x0f)] +pub struct CompletionData { + #[zvt_bmp(number = 0x27)] + pub result_code: Option, + + #[zvt_bmp(number = 0x19)] + pub status_byte: Option, + + #[zvt_bmp(number = 0x29, length = length::Fixed<4>, encoding = encoding::Bcd)] + pub terminal_id: Option, + + #[zvt_bmp(number = 0x49, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x0f)] +pub struct ReceiptPrintoutCompletion { + #[zvt_bmp(length = length::Lllv, encoding = encoding::Utf8)] + pub sw_version: String, + + pub terminal_status_code: u8, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// Resets the terminal. +/// +/// See chapter 2.43. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x18)] +pub struct ResetTerminal {} + +/// See chapter 2.44 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x1a)] +pub struct PrintSystemConfiguration {} + +/// Set/Reset the terminal id. +/// +/// See chapter 2.45. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x1b)] +pub struct SetTerminalId { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: usize, + + #[zvt_bmp(number = 0x29, length = length::Fixed<4>, encoding = encoding::Bcd)] + pub terminal_id: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x1e)] +pub struct Abort { + pub error: u8, +} + +// Defined in 2.2.9 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x1e)] +pub struct ReservationAbort { + pub error: u8, + + // The currency code is here untagged and will be only included if the + // error code above evaluates to 0x6f. + #[zvt_bmp(length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// Defined in 2.10.1. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x1e)] +pub struct PartialReversalAbort { + // Must be [ErrorMessage::ErrorPreAuthorization]. + pub error: u8, + + #[zvt_bmp(number = 0x87, length = length::Fixed<2>, encoding = PartialReversalReceiptNo)] + pub receipt_no: Option, + // TODO(ddo) There is also an tlv field which may contain further receipt + // numbers. Produce the message to understand how it looks like. +} + +/// Pre-Authorization/Reservation. +/// +/// See chapter 2.8. +#[derive(Debug, Default, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x22)] +pub struct Reservation { + #[zvt_bmp(number = 0x04, length = length::Fixed<6>, encoding = encoding::Bcd)] + pub amount: Option, + + #[zvt_bmp(number = 0x49, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x19)] + pub payment_type: Option, + + #[zvt_bmp(number = 0x0e, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub expiry_date: Option, + + #[zvt_bmp(number = 0x22, length = length::Llv, encoding = encoding::Bcd)] + pub card_number: Option, + + // Unclear how to interpret this. + #[zvt_bmp(number = 0x01)] + pub timeout: Option, + + #[zvt_bmp(number = 0x02)] + pub maximum_no_of_status_info: Option, + + #[zvt_bmp(number = 0x05)] + pub pump_no: Option, + + #[zvt_bmp(number = 0x0b, length = length::Fixed<3>, encoding = encoding::Bcd)] + pub trace_number: Option, + + #[zvt_bmp(number = 0x3b, length = length::Fixed<8>)] + pub aid_authorization_attribute: Option, + + #[zvt_bmp(number = 0x3c, length = length::Lllv)] + pub additional_text: Option, + + #[zvt_bmp(number = 0x8a)] + pub zvt_card_type: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// Encoding for receipt-no field in the PartialReversal struct. +/// +/// The field may have a special value - 0xffff - which is not representable in +/// a 2 byte Bcd fashion. See 2.10.1 for details. +pub struct PartialReversalReceiptNo; + +impl encoding::Encoding for PartialReversalReceiptNo { + fn decode(bytes: &[u8]) -> zvt_builder::ZVTResult<(usize, &[u8])> { + if bytes.len() < 2 { + return Err(zvt_builder::ZVTError::IncompleteData); + } + if bytes[0..2] == [0xff, 0xff] { + let tmp: u16 = encoding::Default::decode(&bytes[0..2])?.0; + Ok((tmp as usize, &bytes[2..])) + } else { + Ok((encoding::Bcd::decode(&bytes[0..2])?.0, &bytes[2..])) + } + } + + fn encode(input: &usize) -> Vec { + if *input == 0xffff { + encoding::Default::encode(&(*input as u16)) + } else { + encoding::Bcd::encode(input) + } + } +} + +#[derive(Debug, Default, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x23)] +pub struct PartialReversal { + #[zvt_bmp(number = 0x87, length = length::Fixed<2>, encoding = PartialReversalReceiptNo)] + pub receipt_no: Option, + + #[zvt_bmp(number = 0x04, length = length::Fixed<6>, encoding = encoding::Bcd)] + pub amount: Option, + + #[zvt_bmp(number = 0x19)] + pub payment_type: Option, + + #[zvt_bmp(number = 0x49, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x25)] +pub struct PreAuthReversal { + #[zvt_bmp(number = 0x19)] + pub payment_type: Option, + + #[zvt_bmp(number = 0x49, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub currency: Option, + + #[zvt_bmp(number = 0x87, length = length::Fixed<2>, encoding = encoding::Bcd)] + pub receipt_no: Option, +} + +/// See chapter 2.16 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x50)] +pub struct EndOfDay { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: usize, +} + +/// Defined in 2.17. +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x70)] +pub struct Diagnosis { + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0x93)] +pub struct Initialization { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: usize, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0xc0)] +pub struct ReadCard { + pub timeout_sec: u8, + + #[zvt_bmp(number = 0x19)] + pub card_type: Option, + + #[zvt_bmp(number = 0xfc)] + pub dialog_control: Option, + + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// PrintLine message as defined in 3.5 +/// +/// With this command a printer integrated in or attached to the ECR can be used +/// to print a line from the transferred data. The text contains no CR LF. Empty +/// lines are transferred as print-commands with an empty text-field. +/// The command is only sent from the PT if function ECR-receipt is active on +/// the PT (see command Registration). +/// +/// The ECR shall either respond with [Ack] or [Nack] with (84 cc). +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0xd1)] +pub struct PrintLine { + pub attribute: u8, + + pub text: String, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x06, instr = 0xd3)] +pub struct PrintTextBlock { + #[zvt_bmp(number = 0x06, length = length::Tlv)] + pub tlv: Option, +} + +/// See chapter 2.36 +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x08, instr = 0x30)] +pub struct SelectLanguage { + language: u8, +} + +#[derive(Debug, PartialEq, Zvt)] +#[zvt_control_field(class = 0x80, instr = 0x00)] +pub struct Ack {} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::ZvtSerializer; + use chrono::NaiveDate; + use std::fs; + + pub fn get_bytes(name: &str) -> Vec { + let path_from_root = "rust/zvt/zvt/data/".to_string(); + let base_dir = match fs::metadata(&path_from_root) { + Ok(_) => path_from_root, + Err(_) => format!("data/"), + }; + fs::read(&format!("{base_dir}/{name}")).unwrap() + } + + #[test] + fn test_read_card() { + let bytes = get_bytes("1680722649.972316000_ecr_pt.blob"); + let expected = ReadCard { + timeout_sec: 15, + card_type: Some(16), + dialog_control: Some(2), + tlv: Some(tlv::ReadCard { + card_reading_control: Some(0xd0), + card_type: Some(0x07), + }), + }; + let automatic = expected.zvt_serialize(); + assert_eq!(automatic, bytes); + let output = ReadCard::zvt_deserialize(&bytes).unwrap(); + assert_eq!(expected, output.0); + } + + #[test] + fn test_status_information() { + let bytes = get_bytes("1680728161.963129000_pt_ecr.blob"); + let expected = StatusInformation { + result_code: Some(0), + tlv: Some(tlv::StatusInformation { + uuid: Some("000000000000081ca72f".to_string()), + ats: Some("0578807002".to_string()), + card_type: Some(1), + sub_type: Some("fe04".to_string()), + atqa: Some("0400".to_string()), + sak: Some(0x20), + subs: vec![tlv::Subs { + application_id: Some("a0000000041010".to_string()), + }], + }), + ..StatusInformation::default() + }; + assert_eq!(expected.zvt_serialize(), bytes); + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + + let bytes = get_bytes("1680728165.675509000_pt_ecr.blob"); + let expected = StatusInformation { + result_code: Some(0), + amount: Some(2500), + card_type: Some(0x60), + currency: Some(978), + time: Some(225558), + date: Some(405), + card_number: Some(5598845555548074), + receipt_no: Some(231), + aid_authorization_attribute: Some("750071".to_string()), + trace_number: Some(975), + terminal_id: Some(52523535), + expiry_date: Some(2405), + zvt_card_type: Some(6), + zvt_card_type_id: Some(1), + card_name: Some("MasterCard".to_string()), + vu_number: Some("804011926 ".to_string()), + ..StatusInformation::default() + }; + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + + let bytes = get_bytes("1680728215.659492000_pt_ecr.blob"); + let expected = StatusInformation { + result_code: Some(0), + card_type: Some(0x60), + amount: Some(0), + currency: Some(978), + time: Some(225558), + date: Some(405), + card_number: Some(5598845555548074), + receipt_no: Some(232), + aid_authorization_attribute: Some("750071".to_string()), + trace_number: Some(977), + terminal_id: Some(52523535), + expiry_date: Some(2405), + zvt_card_type: Some(6), + zvt_card_type_id: Some(1), + card_name: Some("MasterCard".to_string()), + vu_number: Some("804011926 ".to_string()), + additional_text: Some( + "AS-Proc-Code= 00 076 06\rCapt.-Ref.= 0099\rAID59= 081520\r DAUER 7 TAGE" + .to_string(), + ), + ..StatusInformation::default() + }; + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + + let bytes = get_bytes("1680761828.489701000_pt_ecr.blob"); + let expected = StatusInformation { + result_code: Some(0), + amount: Some(958), + trace_number: Some(982), + date: Some(406), + time: Some(81706), + single_amounts: Some(SingleAmounts { + receipt_no_start: 233, + receipt_no_end: 234, + eurocard: NumAndTotal { num: 2, total: 958 }, + ..SingleAmounts::default() + }), + ..StatusInformation::default() + }; + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + + // After pre-auth. + let bytes = get_bytes("1682066249.409078000_pt_ecr.blob"); + let expected = StatusInformation { + result_code: Some(0), + amount: Some(2500), + currency: Some(978), + date: Some(421), + time: Some(103720), + card_number: Some(4711008005757038004), + card_sequence_number: Some(2), + receipt_no: Some(249), + aid_authorization_attribute: Some("018372".to_string()), + trace_number: Some(1012), + card_type: Some(0x60), + terminal_id: Some(52523535), + expiry_date: Some(2612), + zvt_card_type: Some(5), + zvt_card_type_id: Some(0), + card_name: Some("girocard".to_string()), + vu_number: Some("16004008 ".to_string()), + ..StatusInformation::default() + }; + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + + let bytes = [ + 4, 15, 100, 39, 0, 6, 96, 76, 10, 0, 0, 0, 0, 0, 0, 8, 255, 105, 20, 31, 69, 12, 12, + 120, 128, 116, 3, 128, 49, 192, 115, 214, 49, 192, 31, 76, 1, 1, 31, 77, 2, 254, 4, 31, + 79, 2, 4, 0, 31, 80, 1, 32, 96, 11, 67, 9, 160, 0, 0, 0, 89, 69, 67, 1, 0, 96, 12, 67, + 10, 160, 0, 0, 3, 89, 16, 16, 2, 128, 1, 96, 11, 67, 9, 210, 118, 0, 0, 37, 71, 65, 1, + 0, 96, 9, 67, 7, 160, 0, 0, 0, 4, 16, 16, + ]; + let expected = StatusInformation::zvt_deserialize(&bytes).unwrap().0; + assert_eq!(expected.tlv.as_ref().unwrap().subs.len(), 4); + assert_eq!(bytes, expected.zvt_serialize().as_slice()); + + let bytes = [ + 4, 15, 34, 39, 0, 6, 30, 76, 10, 0, 0, 0, 4, 99, 200, 178, 174, 79, 128, 31, 76, 1, 1, + 31, 77, 2, 0, 3, 31, 79, 2, 68, 0, 31, 80, 1, 0, + ]; + let expected = StatusInformation::zvt_deserialize(&bytes).unwrap().0; + assert!(expected.tlv.as_ref().unwrap().subs.is_empty()); + } + + #[test] + fn test_receipt_printout_completion() { + let bytes = get_bytes("1680728219.054216000_pt_ecr.blob"); + let expected = ReceiptPrintoutCompletion { + sw_version: "GER-APP-v2.0.9;cS02.01.01-10.10-2-2;CC26".to_string(), + terminal_status_code: 0, + tlv: Some(tlv::ReceiptPrintoutCompletion { + terminal_id: Some(52523535), + device_information: Some(tlv::DeviceInformation { + device_name: Some("cVEND plug".to_string()), + software_version: Some("GER-APP-v2.0.9;cS02.01.01-10.10-2-2;CC26".to_string()), + serial_number: Some(18632442), + device_state: Some(0), + }), + date_time: Some( + NaiveDate::from_ymd_opt(2023, 4, 5) + .unwrap() + .and_hms_opt(22, 56, 55) + .unwrap(), + ), + }), + }; + assert_eq!( + ReceiptPrintoutCompletion::zvt_deserialize(&bytes) + .unwrap() + .0, + expected + ); + } + + #[test] + fn test_pre_auth_data() { + let bytes = get_bytes("1680728162.033575000_ecr_pt.blob"); + let expected = Reservation { + payment_type: Some(0x40), + currency: Some(978), + amount: Some(2500), + tlv: Some(tlv::PreAuthData { + bmp_data: Some(tlv::Bmp60 { + bmp_prefix: "AC".to_string(), + bmp_data: "384HH2".to_string(), + }), + }), + ..Reservation::default() + }; + assert_eq!(Reservation::zvt_deserialize(&bytes).unwrap().0, expected,); + } + + #[test] + fn test_registration() { + let bytes = get_bytes("1681273860.511128000_ecr_pt.blob"); + let expected = Registration { + password: 123456, + config_byte: 0xde, + currency: Some(978), + tlv: None, + }; + assert_eq!(bytes, expected.zvt_serialize()); + assert_eq!(Registration::zvt_deserialize(&bytes).unwrap().0, expected); + } + + #[test] + fn test_pre_auth_reversal() { + let bytes = get_bytes("1680728213.562478000_ecr_pt.blob"); + let expected = PreAuthReversal { + payment_type: Some(0x40), + receipt_no: Some(231), + currency: Some(978), + }; + assert_eq!( + PreAuthReversal::zvt_deserialize(&bytes).unwrap().0, + expected + ); + } + + #[test] + fn test_completion_data() { + let bytes = get_bytes("1680761818.641601000_pt_ecr.blob"); + let golden = CompletionData { + result_code: None, + status_byte: Some(0x10), + terminal_id: Some(52523535), + currency: Some(978), + }; + let out = golden.zvt_serialize(); + assert_eq!(bytes, out); + assert_eq!(CompletionData::zvt_deserialize(&bytes).unwrap().0, golden); + } + + #[test] + fn test_end_of_day() { + let bytes = get_bytes("1681282621.302434000_ecr_pt.blob"); + let expected = EndOfDay { password: 123456 }; + + assert_eq!(EndOfDay::zvt_deserialize(&bytes).unwrap().0, expected); + assert_eq!(bytes, expected.zvt_serialize()); + } + + #[test] + fn test_partial_reversal() { + let bytes = get_bytes("1681455683.221609000_ecr_pt.blob"); + let expected = PartialReversal { + receipt_no: Some(491), + payment_type: Some(0x40), + currency: Some(978), + amount: Some(1295), + tlv: Some(tlv::PreAuthData { + bmp_data: Some(tlv::Bmp60 { + bmp_prefix: "AC".to_string(), + bmp_data: "MF2246".to_string(), + }), + }), + }; + assert_eq!( + PartialReversal::zvt_deserialize(&bytes).unwrap().0, + expected + ); + } + + #[test] + fn test_intermediate_status() { + let bytes = get_bytes("1680728162.647465000_pt_ecr.blob"); + let expected = IntermediateStatusInformation { + status: 0x17, + timeout: Some(0), + }; + assert_eq!( + IntermediateStatusInformation::zvt_deserialize(&bytes) + .unwrap() + .0, + expected + ); + assert_eq!(bytes, expected.zvt_serialize()); + } + + #[test] + fn test_print_text_block() { + let bytes = get_bytes("1680728215.585561000_pt_ecr.blob"); + let actual = PrintTextBlock::zvt_deserialize(&bytes).unwrap().0; + let tlv = actual.tlv.as_ref().unwrap(); + let lines = tlv.lines.as_ref().unwrap(); + assert_eq!(lines.lines.len(), 33); + assert_eq!(lines.eol, Some(255)); + assert_eq!(tlv.receipt_type, Some(2)); + assert_eq!(bytes, actual.zvt_serialize()); + } + + #[test] + fn test_text_block_system_information() { + let bytes = get_bytes("print_system_configuration_reply.blob"); + let actual = PrintTextBlock::zvt_deserialize(&bytes).unwrap().0; + let tlv = actual.tlv.as_ref().unwrap(); + let lines = tlv.lines.as_ref().unwrap(); + assert_eq!(lines.lines.len(), 118); + assert_eq!(lines.eol, None); + assert_eq!(tlv.receipt_type, Some(3)); + assert_eq!(bytes, actual.zvt_serialize()); + } + + #[test] + fn test_partial_reversal_abort() { + let bytes = get_bytes("partial_reversal.blob"); + let actual = PartialReversalAbort::zvt_deserialize(&bytes).unwrap().0; + let expected = PartialReversalAbort { + error: 184, + receipt_no: Some(0xffff), + }; + + assert_eq!(actual, expected); + assert_eq!(expected.zvt_serialize(), bytes); + } +} diff --git a/zvt/src/packets/tlv.rs b/zvt/src/packets/tlv.rs new file mode 100644 index 0000000..549722b --- /dev/null +++ b/zvt/src/packets/tlv.rs @@ -0,0 +1,140 @@ +use crate::{encoding, length, Zvt}; +use chrono::NaiveDateTime; + +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct Subs { + #[zvt_bmp(number = 0x43, length = length::Tlv, encoding = encoding::Hex)] + pub application_id: Option, +} + +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct StatusInformation { + #[zvt_tlv(tag = 0x4c, encoding = encoding::Hex)] + pub uuid: Option, + + #[zvt_tlv(tag = 0x1f45, encoding = encoding::Hex)] + pub ats: Option, + + #[zvt_tlv(tag = 0x1f4c)] + pub card_type: Option, + + #[zvt_tlv(tag = 0x1f4d, encoding = encoding::Hex)] + pub sub_type: Option, + + #[zvt_tlv(tag = 0x1f4f, encoding = encoding::Hex)] + pub atqa: Option, + + #[zvt_tlv(tag = 0x1f50)] + pub sak: Option, + + // The documentation just says that this tag may be present but in reality + // this is a vector. + #[zvt_tlv(tag = 0x60)] + pub subs: Vec, +} + +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct StatusEnquiry { + #[zvt_tlv(tag = 0x1Ff2)] + enable_extended_contactless_card_detection: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct DeviceInformation { + #[zvt_bmp(number = 0x1f40, length = length::Tlv)] + pub device_name: Option, + + #[zvt_bmp(number = 0x1f41, length = length::Tlv)] + pub software_version: Option, + + #[zvt_bmp(number = 0x1f42, length = length::Tlv, encoding = encoding::Bcd)] + pub serial_number: Option, + + #[zvt_bmp(number = 0x1f43, length = length::Tlv)] + pub device_state: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct ReceiptPrintoutCompletion { + #[zvt_tlv(tag = 0x1f44, encoding = encoding::Bcd)] + pub terminal_id: Option, + + #[zvt_tlv(tag = 0xe4)] + pub device_information: Option, + + #[zvt_tlv(tag = 0x34)] + pub date_time: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct ReservationAbort { + // Spec says a "variable length binary". Not sure what this means regarding + // the encoding. + #[zvt_tlv(tag = 0x1f16, encoding = encoding::Bcd)] + pub extended_error_code: Option, + + #[zvt_tlv(tag = 0x1f17)] + pub extended_error_text: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct Bmp60 { + #[zvt_tlv(tag = 0x1f62)] + pub bmp_prefix: String, + + #[zvt_tlv(tag = 0x1f63)] + pub bmp_data: String, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct PreAuthData { + #[zvt_tlv(tag = 0xe9)] + pub bmp_data: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct Diagnosis { + #[zvt_tlv(tag = 0x1b)] + pub diagnosis_type: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct ReadCard { + #[zvt_tlv(tag = 0x1f15)] + pub card_reading_control: Option, + + #[zvt_tlv(tag = 0x1f60)] + pub card_type: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct ZvtString { + #[zvt_bmp(number = 0x07, length=length::Tlv)] + pub line: String, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct TextLines { + #[zvt_tlv(tag = 0x07)] + pub lines: Vec, + + #[zvt_tlv(tag = 0x09)] + pub eol: Option, +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct PrintTextBlock { + #[zvt_tlv(tag = 0x1f07)] + pub receipt_type: Option, + + #[zvt_tlv(tag = 0x25)] + pub lines: Option, + // TODO(ddo) missing 0x14 (ISO character set) and 0x1f37 (Receipt information) +} + +#[derive(Debug, PartialEq, Zvt)] +pub struct Registration { + // Or what means (hi-byte sent before lo-byte) + #[zvt_tlv(tag = 0x1a, encoding = encoding::BigEndian)] + pub max_len_adpu: Option, +} diff --git a/zvt/src/sequences.rs b/zvt/src/sequences.rs new file mode 100644 index 0000000..01670a7 --- /dev/null +++ b/zvt/src/sequences.rs @@ -0,0 +1,692 @@ +use crate::packets; +use crate::{encoding, ZvtEnum, ZvtParser, ZvtSerializer}; +use anyhow::Result; +use async_stream::try_stream; +use futures::Stream; +use log::debug; +use std::boxed::Box; +use std::marker::Unpin; +use std::pin::Pin; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +pub async fn read_packet_async(src: &mut Pin<&mut impl AsyncReadExt>) -> Result> { + let mut buf = vec![0; 3]; + src.read_exact(&mut buf).await?; + + // Get the len. + let len = if buf[2] == 0xff { + buf.resize(5, 0); + src.read_exact(&mut buf[3..5]).await?; + u16::from_le_bytes(buf[3..5].try_into().unwrap()) as usize + } else { + buf[2] as usize + }; + + let start = buf.len(); + buf.resize(start + len, 0); + src.read_exact(&mut buf[start..]).await?; + + debug!("Received {buf:?}"); + + Ok(buf.to_vec()) +} + +#[derive(ZvtEnum)] +enum Ack { + Ack(packets::Ack), +} + +pub async fn write_with_ack_async( + p: &T, + src: &mut Pin<&mut (impl AsyncReadExt + AsyncWriteExt)>, +) -> Result<()> +where + T: ZvtSerializer + Sync + Send, + encoding::Default: encoding::Encoding, +{ + // We declare the bytes as a separate variable to help the compiler to + // figure out that we can send stuff between threads. + let bytes = p.zvt_serialize(); + src.write_all(&bytes).await?; + + let bytes = read_packet_async(src).await?; + let _ = Ack::zvt_parse(&bytes)?; + Ok(()) +} + +/// The trait for converting a sequence into a stream. +/// +/// What is written below? The [Self::Input] type must be a command as defined +/// under [packets]. The [Self::Output] must implement the [ZvtParser] trait - +/// an enum listing all possible packets the PT may send to the ECR after +/// receiving the [Self::Input] message. +/// +/// Additionally we enforce that the [Self::Input] and [Self::Output] are marked +/// with [Send], so the stream can be shared between threads (or moved into +/// [tokio::spawn], e.x.). +/// +/// The default implementation just waits for one message, acknowledges it and +/// returns. +pub trait Sequence +where + Self::Input: ZvtSerializer + Send + Sync, + encoding::Default: encoding::Encoding, +{ + type Input; + type Output: ZvtParser + Send; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + // This pin has nothing to do with the fact that we return a Stream + // but is needed to access methods like `write_all`. + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + let bytes = read_packet_async(&mut src).await?; + let packet = Self::Output::zvt_parse(&bytes)?; + // Write the response. + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + yield packet; + }; + Box::pin(s) + } +} + +/// Registration sequence as defined under 2.1. +/// +/// Using the command Registration the ECR can set up different configurations +/// on the PT and also control the current status of the PT. +pub struct Registration {} + +/// Response to [packets::Registration] as defined under 2.1. +#[derive(Debug, ZvtEnum)] +pub enum RegistrationResponse { + CompletionData(packets::CompletionData), +} + +impl Sequence for Registration { + type Input = packets::Registration; + type Output = RegistrationResponse; +} + +/// Read-card sequence as defined under 2.21. +/// +/// With this command the PT reads a chip-card/magnet-card and transmits the +/// card-data to the ECR. +pub struct ReadCard; + +/// Response to [packets::ReadCard] message as defined in 2.21. +#[derive(Debug, ZvtEnum)] +#[allow(clippy::large_enum_variant)] +pub enum ReadCardResponse { + IntermediateStatusInformation(packets::IntermediateStatusInformation), + StatusInformation(packets::StatusInformation), + Abort(packets::Abort), +} + +impl Sequence for ReadCard { + type Input = packets::ReadCard; + type Output = ReadCardResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = ReadCardResponse::zvt_parse(&bytes)?; + // Write the response. + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + + match packet { + ReadCardResponse::StatusInformation(_) | ReadCardResponse::Abort(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Initialization sequence as defined under 2.18. +/// +/// The command forces the PT to send a initialization message to the ECR. +pub struct Initialization; + +/// Response to [packets::Initialization] message as defined under 2.18. +#[derive(Debug, ZvtEnum)] +pub enum InitializationResponse { + /// 2.18.3 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + /// 2.18.4 + PrintLine(packets::PrintLine), + /// 2.18.4 + PrintTextBlock(packets::PrintTextBlock), + /// 2.18.5, terminal. + CompletionData(packets::CompletionData), + /// 2.18.6, terminal. + Abort(packets::Abort), +} + +impl Sequence for Initialization { + /// Defined under 2.18.1 + type Input = packets::Initialization; + type Output = InitializationResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + // 2.18.1 + write_with_ack_async(input, &mut src).await?; + loop { + let bytes = read_packet_async(&mut src).await?; + let response = InitializationResponse::zvt_parse(&bytes)?; + + // Every message requires an Ack. + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + + match response { + InitializationResponse::CompletionData(_) + | InitializationResponse::Abort(_) => { + yield response; + break; + } + _ => yield response, + } + } + }; + Box::pin(s) + } +} + +/// Set/Reset the terminal id as defined under 2.45. +/// +/// Causes the PT to set or reset the terminal identifier. The command will only +/// be executed, if the turnover storage is empty e.g. after an [EndOfDay] +/// sequence. +pub struct SetTerminalId; + +/// Response to [packets::SetTerminalId] message as defined in 2.45.2 +#[derive(Debug, ZvtEnum)] +pub enum SetTerminalIdResponse { + /// 2.45.2, terminal. + CompletionData(packets::CompletionData), + /// 2.45.2, terminal. + Abort(packets::Abort), +} + +impl Sequence for SetTerminalId { + /// 2.45.1 + type Input = packets::SetTerminalId; + type Output = SetTerminalIdResponse; +} + +/// The Reset-terminal sequence, defined under 2.43. +/// +/// With this command the ECR causes the PT to restart. +pub struct ResetTerminal; + +/// Response to [packets::ResetTerminal] message, defined under 2.43.2. +#[derive(Debug, ZvtEnum)] +pub enum ResetTerminalResponse { + /// 2.43.2, terminal. + CompletionData(packets::CompletionData), +} + +impl Sequence for ResetTerminal { + /// 2.43.1 + type Input = packets::ResetTerminal; + type Output = ResetTerminalResponse; +} + +/// Diagnosis sequence, as defined under 2.17. +/// +/// With this command the ECR forces the PT to send a diagnostic message to the +/// host. +pub struct Diagnosis; + +/// Response to [packets::Diagnosis] message, as defined under 2.17. +#[derive(Debug, ZvtEnum)] +pub enum DiagnosisResponse { + // 2.17.2 not implemented. + /// 2.17.3 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + /// 2.17.4. + /// + /// The message definition can be found under 3.4: + /// + /// If the PT sends this command to the ECR, the ECR sets its system-time to + /// the value sent in Data block. + SetTimeAndDate(packets::SetTimeAndDate), + /// 2.17.5 + PrintLine(packets::PrintLine), + /// 2.17.5 + PrintTextBlock(packets::PrintTextBlock), + /// 2.17.6, terminal. + CompletionData(packets::CompletionData), + /// 2.17.6, terminal. + Abort(packets::Abort), +} + +impl Sequence for Diagnosis { + /// 2.17.1 + type Input = packets::Diagnosis; + type Output = DiagnosisResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + // 2.18.1 + write_with_ack_async(input, &mut src).await?; + loop { + let bytes = read_packet_async(&mut src).await?; + let response = DiagnosisResponse::zvt_parse(&bytes)?; + + // Every message requires an Ack. + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + + match response { + DiagnosisResponse::CompletionData(_) + | DiagnosisResponse::Abort(_) => { + yield response; + break; + } + _ => yield response, + } + } + }; + Box::pin(s) + } +} + +/// End-of-day sequence as defined under 2.16. +/// +/// With this command the ECR induces the PT to transfer the stored turnover to +/// the host. +pub struct EndOfDay; + +/// Response to [packets::EndOfDay] message as defined under 2.16. +#[derive(Debug, ZvtEnum)] +#[allow(clippy::large_enum_variant)] +pub enum EndOfDayResponse { + // TODO(ddo) 2.16.2 not implemented + /// 2.16.3 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + /// 2.16.4 + StatusInformation(packets::StatusInformation), + /// 2.16.5 + PrintLine(packets::PrintLine), + /// 2.16.5 + PrintTextBlock(packets::PrintTextBlock), + /// 2.16.6 + CompletionData(packets::CompletionData), + /// 2.16.6 + /// + /// Our data shows that if there is a pending transaction, the PT rather + /// returns [packets::PartialReversalAbort] over [packets::Abort]. + Abort(packets::PartialReversalAbort), +} + +impl Sequence for EndOfDay { + /// 2.16.1 + type Input = packets::EndOfDay; + type Output = EndOfDayResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + // 2.16.1 + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = EndOfDayResponse::zvt_parse(&bytes)?; + + // Write the response. + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + EndOfDayResponse::CompletionData(_) | EndOfDayResponse::Abort(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Reservation sequence as defined under 2.8. +/// +/// The ECR requests PT to reserve a certain payment-amount. This is necessary +/// when the final payment-amount is only established after the authorization. +/// In this case the ECR firstly reserves an amount (= maximal Possible +/// payment-amount) and then, after the sales-process, releases the unused +/// amount via a [PartialReversal] or Book Total (06 24, not implemented). +pub struct Reservation; + +/// Response to [packets::Reservation] message, as defined under 2.8. +/// +/// The response is the same as for Authorization, defined in chapter 2.1. +#[derive(Debug, ZvtEnum)] +#[allow(clippy::large_enum_variant)] +pub enum AuthorizationResponse { + /// 2.2.4 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + // 2.2.5 produces no message. + /// 2.2.6 + StatusInformation(packets::StatusInformation), + /// 2.2.7 + PrintLine(packets::PrintLine), + /// 2.2.7 + PrintTextBlock(packets::PrintTextBlock), + // 2.2.8 produces no message. + /// 2.2.9 + CompletionData(packets::CompletionData), + /// 2.2.9 + Abort(packets::Abort), +} + +impl Sequence for Reservation { + type Input = packets::Reservation; + type Output = AuthorizationResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + // 2.8 + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = AuthorizationResponse::zvt_parse(&bytes)?; + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + AuthorizationResponse::CompletionData(_) | AuthorizationResponse::Abort(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Partial reversal sequence as defined under 2.10. +/// +/// This command executes a Partial-Reversal for a [Reservation] to release the +/// unused amount of the reservation. The Partial-Reversal is only carried-out +/// if a Pre-Authorization with the passed receipt number (returned in +/// [packets::StatusInformation::receipt_no] after running [Reservation]) is +/// found in the turnover-records. +pub struct PartialReversal; + +/// Response to [packets::PartialReversal] message as defined in 2.10. +/// +/// The output is identical to Reversal (06 30), which has the same output +/// as Authorization (06 01). The only difference is the 2.10.1 case, where +/// we return the currently active transactions. +#[derive(Debug, ZvtEnum)] +#[allow(clippy::large_enum_variant)] +pub enum PartialReversalResponse { + /// 2.2.4 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + // 2.2.5 produces no message. + /// 2.2.6 + StatusInformation(packets::StatusInformation), + /// 2.2.7 + PrintLine(packets::PrintLine), + /// 2.2.7 + PrintTextBlock(packets::PrintTextBlock), + // 2.2.8 produces no message. + /// 2.2.9 + CompletionData(packets::CompletionData), + /// 2.2.9 and 2.10.1 Abort messages. + /// + /// Note: The [packets::Abort] message is a valid subset of + /// [packets::PartialReversalAbort]. + PartialReversalAbort(packets::PartialReversalAbort), +} + +impl Sequence for PartialReversal { + /// 2.10 + type Input = packets::PartialReversal; + type Output = PartialReversalResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = PartialReversalResponse::zvt_parse(&bytes)?; + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + PartialReversalResponse::CompletionData(_) + | PartialReversalResponse::PartialReversalAbort(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Pre-Auth-Reversal sequence as defined in 2.14 +/// +/// This command executes a reversal of a [Reservation] in the case of a +/// null-filling. The sequence is identical to the [PartialReversal]. +pub struct PreAuthReversal; + +impl Sequence for PreAuthReversal { + type Input = packets::PreAuthReversal; + type Output = PartialReversalResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = PartialReversalResponse::zvt_parse(&bytes)?; + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + PartialReversalResponse::CompletionData(_) + | PartialReversalResponse::PartialReversalAbort(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Prints the system information as defined in 2.44. +/// +/// With this command the ECR causes the PT to print its system information to +/// the print target defined in [Registration]. +pub struct PrintSystemConfiguration; + +/// Response to [packets::PrintSystemConfiguration] message, as defined in 2.44. +#[derive(Debug, ZvtEnum)] +pub enum PrintSystemConfigurationResponse { + /// 2.44.2 + PrintLine(packets::PrintLine), + /// 2.44.2 + PrintTextBlock(packets::PrintTextBlock), + /// 2.44.3 + CompletionData(packets::CompletionData), +} + +impl Sequence for PrintSystemConfiguration { + type Input = packets::PrintSystemConfiguration; + type Output = PrintSystemConfigurationResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = PrintSystemConfigurationResponse::zvt_parse(&bytes)?; + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + PrintSystemConfigurationResponse::CompletionData(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} + +/// Sets the language of the PT as defined in 2.36. +/// +/// With this command the ECR selects the language in the PT. +pub struct SelectLanguage; + +/// Response to [packets::SelectLanguage] message as defined in 2.36. +#[derive(Debug, ZvtEnum)] +pub enum SelectLanguageResponse { + CompletionData(packets::CompletionData), +} + +impl Sequence for SelectLanguage { + type Input = packets::SelectLanguage; + type Output = SelectLanguageResponse; +} + +/// Status enquiry sequence as defined in 2.55. +/// +/// With this command the ECR can request the Status of the PT allow the PT to +/// carry out time-controlled events (e.g. OPT-actions or End-of-Day). To allow +/// time-controlled events on the PT to be executed punctually the ECR should +/// send Status-Enquiries as often as possible (every minute or more frequently). +pub struct StatusEnquiry; + +/// Response to [packets::StatusEnquiry] message as defined in 2.55. +#[derive(Debug, ZvtEnum)] +pub enum StatusEnquiryResponse { + // 2.55.2 not supported + /// 2.55.3 + IntermediateStatusInformation(packets::IntermediateStatusInformation), + /// 2.55.4 + PrintLine(packets::PrintLine), + /// 2.55.4 + PrintTextBlock(packets::PrintTextBlock), + /// 2.55.5 + CompletionData(packets::CompletionData), +} + +impl Sequence for StatusEnquiry { + type Input = packets::StatusEnquiry; + type Output = StatusEnquiryResponse; + + fn into_stream<'a, Source>( + input: &'a Self::Input, + src: &'a mut Source, + ) -> Pin> + Send + 'a>> + where + Source: AsyncReadExt + AsyncWriteExt + Unpin + Send, + Self: 'a, + { + let s = try_stream! { + tokio::pin!(src); + write_with_ack_async(input, &mut src).await?; + + loop { + let bytes = read_packet_async(&mut src).await?; + let packet = StatusEnquiryResponse::zvt_parse(&bytes)?; + src.write_all(&packets::Ack {}.zvt_serialize()).await?; + match packet { + StatusEnquiryResponse::CompletionData(_) => { + yield packet; + break; + } + _ => yield packet, + } + } + }; + Box::pin(s) + } +} diff --git a/zvt/tests/derive.rs b/zvt/tests/derive.rs new file mode 100644 index 0000000..8e64fb6 --- /dev/null +++ b/zvt/tests/derive.rs @@ -0,0 +1,234 @@ +use zvt::{encoding, length, Tag, ZVTError, Zvt, ZvtSerializer, ZvtSerializerImpl}; + +#[test] +#[rustfmt::skip] +fn test_zvt_serializer_impl() { + type Input = u16; + // Test without length and tag. + assert_eq!(::serialize_tagged(&12, None), [12, 0]); + assert_eq!(::deserialize_tagged(&[12, 0], None).unwrap().0, 12); + + // Tests with length and custom encoding. + assert_eq!(>::serialize_tagged(&12, None), [2, 12, 0]); + assert_eq!(>::deserialize_tagged(&[2, 12, 0], None).unwrap().0, 12); + assert_eq!(>::serialize_tagged(&12, None), [2, 12, 0]); + assert_eq!(>::deserialize_tagged(&[2, 12, 0], None).unwrap().0, 12); + assert_eq!(>::serialize_tagged(&12, None), [1, 0x12]); + assert_eq!(>::deserialize_tagged(&[1, 0x12], None).unwrap().0, 12); + + // Test with length and tags. + assert_eq!(::serialize_tagged(&12, Some(Tag(11))), [11, 12, 0]); + assert_eq!(::deserialize_tagged(&[11, 12, 0], Some(Tag(11))).unwrap().0, 12); + assert_eq!(>::serialize_tagged(&12, Some(Tag(11))), [11, 2, 12, 0]); + assert_eq!(>::deserialize_tagged(&[11, 2, 12, 0], Some(Tag(11))).unwrap().0, 12); + assert_eq!(>::serialize_tagged(&12, Some(Tag(11))), [0, 11, 2, 0, 12]); + assert_eq!(>::deserialize_tagged(&[0, 11, 2, 0, 12], Some(Tag(11))).unwrap().0, 12); + + // Test failures + assert_eq!(::deserialize_tagged(&[1], None), Err(ZVTError::IncompleteData)); + assert_eq!(::deserialize_tagged(&[12, 12, 0], Some(Tag(11))), Err(ZVTError::WrongTag(Tag(12)))); +} + +#[test] +#[rustfmt::skip] +fn test_zvt_serializer_impl_option() { + // Tests the optional logic of the ZvtSerializerImpl. + type Input = Option; + // Test without a tag. + assert_eq!(::serialize_tagged(&Some(12), None), [12, 0]); + assert_eq!(::deserialize_tagged(&[12, 0], None).unwrap().0, Some(12)); + + assert_eq!(::serialize_tagged(&None, None), []); + assert_eq!(::deserialize_tagged(&[0], None).unwrap().0, None); + + // Test with a tag. + assert_eq!(::serialize_tagged(&Some(12), Some(Tag(11))), [11, 12, 0]); + assert_eq!(::deserialize_tagged(&[11, 12, 0], Some(Tag(11))).unwrap().0, Some(12)); + + assert_eq!(::serialize_tagged(&None, Some(Tag(11))), []); + assert_eq!(::deserialize_tagged(&[], Some(Tag(11))), Err(ZVTError::IncompleteData)); + assert_eq!(::deserialize_tagged(&[0], Some(Tag(11))), Err(ZVTError::WrongTag(Tag(0)))); +} + +#[test] +fn test_command() { + #[derive(Zvt, PartialEq, Debug)] + #[zvt_control_field(class = 13, instr = 12)] + struct Bar { + a: u8, + + #[zvt_bmp(number = 0x3, encoding = encoding::BigEndian)] + b: Option, + + #[zvt_bmp(number = 0x1)] + c: u16, + + #[zvt_bmp(number = 0x2)] + d: Option, + } + + // Some legitimate values. + let values = [ + ( + Bar { + a: 1, + b: Some(2), + c: 3, + d: None, + }, + vec![13, 12, 9, 1, 3, 0, 0, 0, 2, 1, 3, 0], + ), + ( + Bar { + a: 1, + b: None, + c: 3, + d: Some(4), + }, + vec![13, 12, 9, 1, 1, 3, 0, 2, 4, 0, 0, 0], + ), + ]; + + for (input, bytes) in values { + assert_eq!(input.zvt_serialize(), bytes); + assert_eq!(Bar::zvt_deserialize(&bytes).unwrap().0, input); + } + + // The tagged values but not in order. + let values = [ + vec![13, 12, 14, 1, 2, 4, 0, 0, 0, 1, 3, 0, 3, 0, 0, 0, 2], + vec![13, 12, 14, 1, 2, 4, 0, 0, 0, 3, 0, 0, 0, 2, 1, 3, 0], + ]; + + let expected = Bar { + a: 1, + b: Some(2), + c: 3, + d: Some(4), + }; + for bytes in values { + assert_eq!(Bar::zvt_deserialize(&bytes).unwrap().0, expected); + } + + // Missing required field c + let bytes = vec![13, 12, 11, 1, 2, 4, 0, 0, 0, 3, 0, 0, 0, 2]; + assert_eq!( + Bar::zvt_deserialize(&bytes), + Err(ZVTError::MissingRequiredTags(vec![Tag(0x1)])) + ); + + // Duplicate field b + let bytes = vec![13, 12, 14, 1, 3, 0, 0, 0, 2, 1, 3, 0, 3, 0, 0, 0, 2]; + assert_eq!( + Bar::zvt_deserialize(&bytes), + Err(ZVTError::DuplicateTag(Tag(3))) + ); +} + +#[test] +fn test_encodings() { + #[derive(Zvt, Debug, PartialEq)] + struct Foo { + #[zvt_bmp(encoding = encoding::BigEndian)] + a: u16, + + #[zvt_bmp(length = length::Tlv, encoding = encoding::Bcd)] + b: u32, + + #[zvt_bmp(encoding = encoding::Hex)] + c: String, + } + + let input = Foo { + a: 1, + b: 2, + c: "ffde".to_string(), + }; + + let expected_bytes = vec![0, 1, 1, 2, 0xff, 0xde]; + let bytes = input.zvt_serialize(); + assert_eq!(bytes, expected_bytes); + assert_eq!(Foo::zvt_deserialize(&expected_bytes).unwrap().0, input); +} + +#[test] +fn test_named_struct() { + #[derive(Zvt, Debug, PartialEq)] + struct MyNamedStruct { + x: u8, + y: u32, + } + let d = MyNamedStruct { x: 1, y: 2 }; + let expected_bytes = vec![1, 2, 0, 0, 0]; + assert_eq!(expected_bytes, d.zvt_serialize()); + let deser = MyNamedStruct::zvt_deserialize(&expected_bytes).unwrap(); + assert_eq!(deser.0, d); + assert!(deser.1.is_empty()) +} + +#[test] +fn test_foo() { + #[derive(Zvt, PartialEq, Debug)] + struct Foo { + #[zvt_bmp(length = length::Fixed<2>, encoding = encoding::Bcd)] + a: usize, + + #[zvt_bmp(number = 0x13, length = length::Fixed<4>, encoding = encoding::Bcd)] + b: Option, + } + let f = Foo { a: 1, b: None }; + let bytes = f.zvt_serialize(); + let o = Foo::zvt_deserialize(bytes.as_slice()).unwrap(); + assert_eq!(f, o.0); +} + +#[test] +fn test_nested() { + #[derive(Zvt, PartialEq, Debug)] + struct Inner { + a: u8, + #[zvt_bmp(number = 0x12)] + b: Option, + } + + #[derive(Zvt, PartialEq, Debug)] + struct Outer { + a: u16, + #[zvt_bmp(number = 0x12, length = length::Tlv)] + b: Option, + } + + let f = Outer { + a: 1, + b: Some(Inner { a: 2, b: Some(3) }), + }; + let bytes = f.zvt_serialize(); + let o = Outer::zvt_deserialize(bytes.as_slice()).unwrap(); + assert_eq!(f, o.0); +} + +#[test] +fn test_tagged_but_optional_but_still_tagged() { + #[derive(Zvt, PartialEq, Debug)] + struct Foo { + #[zvt_bmp(number = 0x11)] + a: u16, + #[zvt_bmp(number = 0x12)] + b: Option, + } + + // The simple case + for f in [Foo { a: 1, b: Some(2) }, Foo { a: 1, b: None }] { + let bytes = f.zvt_serialize(); + let o = Foo::zvt_deserialize(bytes.as_slice()).unwrap(); + assert_eq!(f, o.0); + } + + // Now check the case where the required field is missing. + for data in [vec![], vec![0x12, 0, 0]] { + assert_eq!( + Foo::zvt_deserialize(&data), + Err(ZVTError::MissingRequiredTags(vec![Tag(0x11)])) + ); + } +} diff --git a/zvt_builder/BUILD.bazel b/zvt_builder/BUILD.bazel new file mode 100644 index 0000000..a121886 --- /dev/null +++ b/zvt_builder/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_builder", + srcs = glob(["src/**/*.rs"]), + edition = "2021", + proc_macro_deps = all_crate_deps(proc_macro = True) + ["//zvt_derive"], + visibility = ["//zvt:__pkg__"], + deps = all_crate_deps(normal = True), +) + +rust_test( + name = "zvt_builder_test", + srcs = [], + crate = ":zvt_builder", +) diff --git a/zvt_builder/Cargo.toml b/zvt_builder/Cargo.toml new file mode 100644 index 0000000..d48fb08 --- /dev/null +++ b/zvt_builder/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zvt_builder" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.26" +hex = "0.4.3" +log = "0.4.20" +thiserror = "1.0.43" +yore = "1.0.2" +zvt_derive = { version = "0.1.0", path = "../zvt_derive" } diff --git a/zvt_builder/src/encoding.rs b/zvt_builder/src/encoding.rs new file mode 100644 index 0000000..f5065ec --- /dev/null +++ b/zvt_builder/src/encoding.rs @@ -0,0 +1,376 @@ +use super::*; +use chrono::{NaiveDate, NaiveDateTime}; +use hex::{FromHex, ToHex}; +use std::mem::size_of; +use yore::code_pages::CP437; + +/// The base trait for encoding. +/// +/// This trait is the working horse of the whole serialization: It takes a value +/// and serializes/deserializes it. +/// +/// # Serialization +/// +/// The input is the value type, the output are bytes generated by a specific +/// encoding. +/// +/// # Deserialization +/// +/// The input are bytes and the output is either an error or a tuple containing +/// the deserialized value and the remaining data. +pub trait Encoding { + fn encode(input: &T) -> Vec; + fn decode(bytes: &[u8]) -> ZVTResult<(T, &[u8])>; +} + +/// Marker for Default encoding. +/// +/// This is for most cases the default encoding of the integral data types. +pub struct Default; + +/// Macro for encoding integral types. +macro_rules! encode_integral { + ($ty:ty, $name:ident, $encode:ident, $decode:ident) => { + impl Encoding<$ty> for $name { + fn encode(input: &$ty) -> Vec { + input.$encode().to_vec() + } + + fn decode(data: &[u8]) -> ZVTResult<($ty, &[u8])> { + let size = size_of::<$ty>(); + + if data.len() < size { + Err(ZVTError::IncompleteData) + } else { + let bytes = data[0..size] + .try_into() + .map_err(|_| ZVTError::IncompleteData)?; + let res = <$ty>::$decode(bytes); + Ok((res, &data[size..])) + } + } + } + }; +} + +// Register all unsigned integral data types. +encode_integral!(u8, Default, to_le_bytes, from_le_bytes); +encode_integral!(u16, Default, to_le_bytes, from_le_bytes); +encode_integral!(u32, Default, to_le_bytes, from_le_bytes); +encode_integral!(u64, Default, to_le_bytes, from_le_bytes); +encode_integral!(usize, Default, to_le_bytes, from_le_bytes); + +/// Default encoding for [String]. +/// +/// The implementation of [decode] consumes the entire byte array. +impl Encoding for Default { + fn encode(input: &String) -> Vec { + let res = CP437.encode(input).unwrap(); + res.try_into().unwrap() + } + + fn decode(data: &[u8]) -> ZVTResult<(String, &[u8])> { + Ok(( + CP437.decode(data).trim_end_matches(0u8 as char).to_string(), + &[], + )) + } +} + +/// Default encoding for [NaiveDateTime]. +impl Encoding for Default { + fn encode(_: &NaiveDateTime) -> Vec { + vec![] + } + + fn decode(mut data: &[u8]) -> ZVTResult<(NaiveDateTime, &[u8])> { + let mut date = usize::default(); + let mut time = u32::default(); + let mut seen_tags = std::collections::HashSet::::new(); + const DATE_TAG: u16 = 0x1f0e; + const TIME_TAG: u16 = 0x1f0f; + while !data.is_empty() { + // Get the tag. + let tag: Tag = Default::decode(data)?.0; + match tag.0 { + DATE_TAG => { + if !seen_tags.insert(DATE_TAG) { + return Err(ZVTError::DuplicateTag(Tag(DATE_TAG))); + } + // Get the date. + (date, data) = >::deserialize_tagged( + data, Some(Tag(DATE_TAG)) + )?; + } + TIME_TAG => { + if !seen_tags.insert(TIME_TAG) { + return Err(ZVTError::DuplicateTag(Tag(TIME_TAG))); + } + (time, data) = + >::deserialize_tagged( + data, + Some(Tag(TIME_TAG)), + )?; + } + _ => break, + } + } + // We haven't found everything we wanted. + if seen_tags.len() != 2 { + return Err(ZVTError::IncompleteData); + } + + Ok(( + NaiveDate::from_ymd_opt( + date as i32 / 10000, + (date as u32 % 1000) / 100, + date as u32 % 100, + ) + .unwrap() + .and_hms_opt(time / 10000, (time % 10000) / 100, time % 100) + .ok_or(ZVTError::IncompleteData)?, + data, + )) + } +} + +/// Default encoding for [Tag]. +/// +/// The default is when the [Tag] is used as a Bmp-number or as a Tlv-tag. +impl encoding::Encoding for Default { + fn encode(input: &Tag) -> Vec { + if (input.0 >> 8) == 0x1f { + input.0.to_be_bytes().to_vec() + } else { + vec![input.0 as u8] + } + } + + fn decode(bytes: &[u8]) -> ZVTResult<(Tag, &[u8])> { + let (tag, new_bytes): (u8, _) = encoding::BigEndian::decode(bytes)?; + if tag == 0x1f { + if bytes.len() < 2 { + Err(ZVTError::IncompleteData) + } else { + let (tag, new_bytes): (u16, _) = encoding::BigEndian::decode(bytes)?; + Ok((Tag(tag), new_bytes)) + } + } else { + Ok((Tag(tag as u16), new_bytes)) + } + } +} + +impl Encoding> for E +where + E: Encoding, +{ + fn decode(data: &[u8]) -> ZVTResult<(Option, &[u8])> + where + Self: Sized, + { + match E::decode(data) { + Err(err) => Err(err), + Ok(data) => Ok((Some(data.0), data.1)), + } + } + + fn encode(input: &Option) -> Vec { + match input { + None => vec![], + Some(inner) => E::encode(inner), + } + } +} + +/// Blanket encoding/decoding for a [Vec]. +/// +/// The encoder will just encode every item and flatten the result. The decoder +/// will decode the entire input into a [Vec]. +impl Encoding> for E +where + E: Encoding, +{ + fn encode(input: &Vec) -> Vec { + input.iter().flat_map(|item| E::encode(item)).collect() + } + + fn decode(mut bytes: &[u8]) -> ZVTResult<(Vec, &[u8])> { + let mut out = Vec::new(); + while !bytes.is_empty() { + let (res, tmp) = E::decode(bytes)?; + bytes = tmp; + out.push(res); + } + Ok((out, bytes)) + } +} + +/// Marker for big endian encoding. +/// +/// Some formats (e.x. feig) require big endian encoding. The semantics are the +/// same as in [Default]. +pub struct BigEndian; + +// Register all unsigned integral data types. +encode_integral!(u8, BigEndian, to_be_bytes, from_be_bytes); +encode_integral!(u16, BigEndian, to_be_bytes, from_be_bytes); +encode_integral!(u32, BigEndian, to_be_bytes, from_be_bytes); +encode_integral!(u64, BigEndian, to_be_bytes, from_be_bytes); +encode_integral!(usize, BigEndian, to_be_bytes, from_be_bytes); + +impl encoding::Encoding for BigEndian { + fn encode(input: &Tag) -> Vec { + encoding::BigEndian::encode(&input.0) + } + + fn decode(bytes: &[u8]) -> ZVTResult<(Tag, &[u8])> { + let res: (u16, _) = encoding::BigEndian::decode(bytes)?; + Ok((Tag(res.0), res.1)) + } +} + +/// Marker for Bcd encoding. +/// +/// See https://en.wikipedia.org/wiki/Binary-coded_decimal for further details. +/// +/// The implementation of [decode] consumes the entire byte array. +pub struct Bcd; + +macro_rules! bcd_integrals { + ($ty:ty) => { + impl Encoding<$ty> for Bcd { + fn encode(input: &$ty) -> Vec { + let mut k = *input; + let mut rv = vec![]; + while k != 0 { + let mut curr = (k % 10) as u8; + k /= 10; + curr |= ((k % 10) as u8) << 4; + k /= 10; + rv.push(curr); + } + rv.reverse(); + rv + } + + fn decode(data: &[u8]) -> ZVTResult<($ty, &[u8])> { + let mut rv = 0; + for d in data.iter() { + let high = (d >> 4) as $ty; + let low = (d & 0xf) as $ty; + if low != 0xf { + rv = (rv * 100) + ((d >> 4) as $ty * 10) + low; + } else { + rv = (rv * 10) + high; + } + } + Ok((rv, &[])) + } + } + }; +} + +bcd_integrals!(u8); +bcd_integrals!(u16); +bcd_integrals!(u32); +bcd_integrals!(u64); +bcd_integrals!(usize); + +pub struct Hex; + +impl Encoding for Hex { + fn encode(input: &String) -> Vec { + >::from_hex(input.clone().as_bytes()).unwrap() + } + + fn decode(data: &[u8]) -> ZVTResult<(String, &[u8])> { + Ok((data.encode_hex(), &[])) + } +} + +pub struct Utf8; + +impl Encoding for Utf8 { + fn encode(_: &String) -> Vec { + vec![] + } + + fn decode(data: &[u8]) -> ZVTResult<(String, &[u8])> { + let string = String::from_utf8(data.to_vec()).map_err(|_| ZVTError::IncompleteData)?; + Ok((string, &[])) + } +} + +/// Macro for registering the basic types defined here as a ZvtSerializerImpl +/// trait. +macro_rules! zvt_serializer_registry { + ($ty:ty) => { + impl, TE: encoding::Encoding> + ZvtSerializerImpl for $ty + { + } + }; +} + +zvt_serializer_registry!(u8); +zvt_serializer_registry!(u16); +zvt_serializer_registry!(u32); +zvt_serializer_registry!(u64); +zvt_serializer_registry!(usize); +zvt_serializer_registry!(String); +zvt_serializer_registry!(NaiveDateTime); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_default() { + // Simple encoding. + assert_eq!(Default::encode(&1234u16), [210, 4]); + assert_eq!(Default::encode(&1234u32), [210, 4, 0, 0]); + assert_eq!(Default::encode(&1234u64), [210, 4, 0, 0, 0, 0, 0, 0]); + + // Simple decoding. + let a: u16 = Default::decode(&[210, 4]).unwrap().0; + assert_eq!(a, 1234); + let a: u32 = Default::decode(&[210, 4, 0, 0]).unwrap().0; + assert_eq!(a, 1234); + + let a: u64 = Default::decode(&[210, 4, 0, 0, 0, 0, 0, 0]).unwrap().0; + assert_eq!(a, 1234); + + // Errors + let a: ZVTResult<(u16, _)> = Default::decode(&[1]); + assert_eq!(a, Err(ZVTError::IncompleteData)); + } + + #[test] + fn test_big_endian() { + assert_eq!(BigEndian::encode(&1234u16), [4, 210]); + assert_eq!(BigEndian::encode(&1234u32), [0, 0, 4, 210]); + assert_eq!(BigEndian::encode(&1234u64), [0, 0, 0, 0, 0, 0, 4, 210]); + + let a: u16 = BigEndian::decode(&[4, 210]).unwrap().0; + assert_eq!(a, 1234); + let a: u32 = BigEndian::decode(&[0, 0, 4, 210]).unwrap().0; + assert_eq!(a, 1234); + + let a: u64 = BigEndian::decode(&[0, 0, 0, 0, 0, 0, 4, 210]).unwrap().0; + assert_eq!(a, 1234); + + let a: ZVTResult<(u32, _)> = BigEndian::decode(&[1, 2, 3]); + assert_eq!(a, Err(ZVTError::IncompleteData)); + } + + #[test] + fn test_bcd() { + assert_eq!(Bcd::encode(&1234u16), [0x12, 0x34]); + let a: u16 = Bcd::decode(&[0x12, 0x34]).unwrap().0; + assert_eq!(a, 1234); + } +} diff --git a/zvt_builder/src/length.rs b/zvt_builder/src/length.rs new file mode 100644 index 0000000..62eb223 --- /dev/null +++ b/zvt_builder/src/length.rs @@ -0,0 +1,160 @@ +use super::encoding::{Default, Encoding}; +use super::*; + +pub trait Length { + fn serialize(len: usize) -> Vec; + fn deserialize(bytes: &[u8]) -> ZVTResult<(usize, &[u8])>; +} + +/// Marker for the case where we don't encode the length. +/// +/// This length type is applied by default. It does not serialize anything +/// and does not consume any data. +pub struct Empty; + +impl Length for Empty { + fn serialize(_: usize) -> Vec { + vec![] + } + + fn deserialize(bytes: &[u8]) -> ZVTResult<(usize, &[u8])> { + Ok((bytes.len(), bytes)) + } +} + +/// Marker for fixed length. +/// +/// A fixed length pads the input to the desired length. The deserialization +/// will always return the fixed value and the full data. +pub struct Fixed(pub usize); + +impl Length for Fixed { + fn serialize(len: usize) -> Vec { + vec![0; N - len] + } + + fn deserialize(data: &[u8]) -> ZVTResult<(usize, &[u8])> { + if data.len() < N { + return Err(ZVTError::IncompleteData); + } + Ok((N, data)) + } +} + +/// Marker for Tlv data types. +pub struct Tlv; + +impl Length for Tlv { + fn serialize(len: usize) -> Vec { + const U8_MAX: usize = u8::MAX as usize; + const U16_MAX: usize = u16::MAX as usize; + + match len { + 0..=127 => vec![len as u8], + 128..=U8_MAX => vec![0x81, len as u8], + 256..=U16_MAX => { + let bytes = (len as u16).to_be_bytes().to_vec(); + [vec![0x82], bytes].concat() + } + _ => panic!("Unsupported length"), + } + } + + fn deserialize(data: &[u8]) -> ZVTResult<(usize, &[u8])> { + let Some(d) = data.first() + else { + return Err(ZVTError::IncompleteData); + }; + + match d { + 0..=127 => Ok((*d as usize, &data[1..])), + 0x81 => { + if let Some(d) = data.get(1) { + Ok((*d as usize, &data[2..])) + } else { + Err(ZVTError::IncompleteData) + } + } + 0x82 => { + let bytes: [u8; 2] = data[1..3] + .try_into() + .map_err(|_| ZVTError::IncompleteData)?; + Ok((u16::from_be_bytes(bytes) as usize, &data[3..])) + } + _ => Err(ZVTError::NonImplemented), + } + } +} + +/// The LLV format is just uncompressed BCD. +pub struct LlvImpl; + +impl Length for LlvImpl { + fn serialize(input: usize) -> Vec { + let mut k = input; + let mut rv = vec![0; N]; + for i in (0..N).rev() { + rv[i] = (k % 10) as u8; + k /= 10; + } + rv + } + + fn deserialize(data: &[u8]) -> ZVTResult<(usize, &[u8])> { + let mut rv = 0; + for i in 0..N { + let Some(d) = data.get(i) + else { + return Err(ZVTError::IncompleteData); + }; + let d = (d & 0xf) as usize; + rv = rv * 10 + d; + } + Ok((rv, &data[N..])) + } +} + +pub type Llv = LlvImpl<2>; +pub type Lllv = LlvImpl<3>; + +pub struct Adpu; + +impl Length for Adpu { + fn serialize(input: usize) -> Vec { + if input < 0xff { + vec![input as u8] + } else { + let res = Default::encode(&(input as u16)); + [vec![0xff], res].concat() + } + } + + fn deserialize(data: &[u8]) -> ZVTResult<(usize, &[u8])> { + let Some(d) = data.first() + else { + return Err(ZVTError::IncompleteData); + }; + + if *d == 0xff { + let res: (u16, _) = Default::decode(&data[1..])?; + Ok((res.0 as usize, res.1)) + } else { + Ok((*d as usize, &data[1..])) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_tlv() { + let data = vec![0, 127, 255, 256, 300]; + + for d in data { + let bytes = Tlv::serialize(d as usize); + let (output, _) = Tlv::deserialize(&bytes).unwrap(); + assert_eq!(d, output, "{:?}", bytes); + } + } +} diff --git a/zvt_builder/src/lib.rs b/zvt_builder/src/lib.rs new file mode 100644 index 0000000..a622765 --- /dev/null +++ b/zvt_builder/src/lib.rs @@ -0,0 +1,233 @@ +use encoding::Encoding; +use log::debug; +use thiserror::Error; + +pub mod encoding; +pub mod length; + +#[derive(Debug, PartialEq, Error)] +pub enum ZVTError { + #[error("Incomplete data")] + IncompleteData, + + #[error("The following tags are required, but were missing: {0:?}")] + MissingRequiredTags(Vec), + + #[error("Not implemented")] + NonImplemented, + + #[error("Unexpected tag: {0:?}")] + WrongTag(Tag), + + #[error("Duplicate tag: {0:?}")] + DuplicateTag(Tag), + + #[error("Received an abort {0}")] + Aborted(u8), +} + +pub type ZVTResult = ::std::result::Result; + +/// The tag of a field. +/// +/// The tag is equivalent to the bmp-number in the Zvt documentation. +#[derive(Debug, PartialEq, Clone)] +pub struct Tag(pub u16); + +/// Trait for commands. +/// +/// The trait encodes the control fields of an Adpu package. +pub trait ZvtCommand { + const CLASS: u8; + const INSTR: u8; +} + +pub trait NotZvtCommand {} + +/// The implementation for serializing/deserializing a Zvt struct. +/// +/// Trait implements the basic logic of the Zvt protocol. Every package consists +/// of up to three fields: +/// +/// `` `` ``. +/// +/// The BMP-NUMBER and LENGTH are optional; The DATA may be encoded in different +/// ways. +/// +/// # Parameters: +/// +/// * `L`: The trait [length::Length] encodes/decodes the `` field. +/// Use [length::Empty] to omit the length. +/// * `E`: The trait [encoding::Encoding] encodes/decodes the given data an +/// generates the DATA field. In order to use this trait with custom +/// types, you have to implement the [encoding::Encoding] trait for your type. +/// * `TE`: The trait [encoding::Encoding] which encodes/decodes the [Tag] +/// the `` field. +pub trait ZvtSerializerImpl< + L: length::Length = length::Empty, + E: encoding::Encoding = encoding::Default, + TE: encoding::Encoding = encoding::Default, +> where + Self: Sized, +{ + fn serialize_tagged(&self, tag: Option) -> Vec { + let mut output = Vec::new(); + if let Some(tag) = tag { + output = TE::encode(&tag); + } + let mut payload = E::encode(self); + let mut length = L::serialize(payload.len()); + output.append(&mut length); + output.append(&mut payload); + output + } + + fn deserialize_tagged(mut bytes: &[u8], tag: Option) -> ZVTResult<(Self, &[u8])> { + if let Some(desired_tag) = tag { + let actual_tag; + (actual_tag, bytes) = TE::decode(bytes)?; + if actual_tag != desired_tag { + return Err(ZVTError::WrongTag(actual_tag)); + } + debug!( + "found tag: 0x{:x}, remaining bytes after tag: {:x?}", + actual_tag.0, bytes + ); + } + let (length, payload) = L::deserialize(bytes)?; + if length > payload.len() { + return Err(ZVTError::IncompleteData); + } + let (data, remainder) = E::decode(&payload[..length])?; + + Ok((data, &payload[length - remainder.len()..])) + } +} + +/// The implementation for serializing/deserializing a optional Zvt fields. +/// +/// Optional fields don't generate anything if the data field is [None]. They +/// generate the same output if the data field is provided. +/// +/// When deserializing optional fields behave differently depending if a [Tag] +/// is provided or not. If the [Tag] is [None] the deserialization always +/// succeeds returning [None] as the result. If the [Tag] is provided, then the +/// serialization error is propagated. +impl, TE: encoding::Encoding> + ZvtSerializerImpl for Option +where + T: ZvtSerializerImpl, +{ + fn serialize_tagged(&self, tag: Option) -> Vec { + // If the data is missing, the tag is also missing. + match self { + None => Vec::new(), + Some(ref data) => >::serialize_tagged(data, tag), + } + } + + fn deserialize_tagged(bytes: &[u8], tag: Option) -> ZVTResult<(Self, &[u8])> { + // If we're deserializing and the tag is a match but the data is still + // missing, this is an error. + match &tag { + Some(_) => match >::deserialize_tagged(bytes, tag) { + Err(err) => Err(err), + Ok(data) => Ok((Some(data.0), data.1)), + }, + None => match >::deserialize_tagged(bytes, None) { + Err(_) => Ok((None, bytes)), + Ok(data) => Ok((Some(data.0), data.1)), + }, + } + } +} + +/// The implementation for serializing/deserializing [Vec] +/// +/// The Zvt protocol does not define a consistent handling of vectors. However, +/// in most cases it assumes that every element is tagged and not the vector +/// itself. Therefore we provide a default serialization/deserialization which +/// does exactly this. +/// +/// The serialization will return an empty vector for an empty input. Otherwise +/// it will tag every element independently and collect the results. +/// +/// The deserialization will deserialize the input until a failure occurs - +/// this indicates that there are no more elements in the vector - and return +/// the results. This means that all elements in the vector must be placed +/// consecutively to each other. +impl, TE: encoding::Encoding> + ZvtSerializerImpl for Vec +where + T: ZvtSerializerImpl, +{ + fn serialize_tagged(&self, tag: Option) -> Vec { + self.iter() + .flat_map(|item| { + >::serialize_tagged(item, tag.clone()) + }) + .collect() + } + + fn deserialize_tagged(mut bytes: &[u8], tag: Option) -> ZVTResult<(Self, &[u8])> { + let mut items = Vec::new(); + + while let Ok((item, remainder)) = + >::deserialize_tagged(bytes, tag.clone()) + { + items.push(item); + bytes = remainder; + } + + Ok((items, bytes)) + } +} + +/// Serializes/Deserializes an Zvt packet. +/// +/// The trait wraps the ZvtSerializerImpl and allows a simple serialization/ +/// deserialization. +pub trait ZvtSerializer: ZvtSerializerImpl +where + Self: Sized, + encoding::Default: encoding::Encoding, +{ + fn zvt_serialize(&self) -> Vec { + ::serialize_tagged(self, None) + } + + fn zvt_deserialize(bytes: &[u8]) -> ZVTResult<(Self, &[u8])> { + ::deserialize_tagged(bytes, None) + } +} + +/// Serializes/Deserializes an Adpu packet. +impl ZvtSerializer for T +where + Self: ZvtCommand + + ZvtSerializerImpl + + ZvtSerializerImpl, + encoding::Default: encoding::Encoding, +{ + fn zvt_serialize(&self) -> Vec { + // Find a more elegant way to express this. + let tag: Tag = encoding::BigEndian::decode(&[Self::CLASS, Self::INSTR]) + .unwrap() + .0; + >::serialize_tagged(self, Some(tag)) + } + + fn zvt_deserialize(bytes: &[u8]) -> ZVTResult<(Self, &[u8])> { + let tag: Tag = encoding::BigEndian::decode(&[Self::CLASS, Self::INSTR]) + .unwrap() + .0; + >::deserialize_tagged(bytes, Some(tag)) + } +} + +pub trait ZvtParser +where + Self: Sized, +{ + fn zvt_parse(bytes: &[u8]) -> ZVTResult; +} diff --git a/zvt_derive/BUILD.bazel b/zvt_derive/BUILD.bazel new file mode 100644 index 0000000..4cf908b --- /dev/null +++ b/zvt_derive/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_proc_macro", "rust_test") +load("@crate_index//:defs.bzl", "all_crate_deps") + +rust_proc_macro( + name = "zvt_derive", + srcs = ["src/lib.rs"], + edition = "2021", + proc_macro_deps = all_crate_deps(proc_macro = True), + visibility = [ + "//zvt:__pkg__", + "//zvt_builder:__pkg__", + ], + deps = all_crate_deps(), +) diff --git a/zvt_derive/Cargo.toml b/zvt_derive/Cargo.toml new file mode 100644 index 0000000..691eb75 --- /dev/null +++ b/zvt_derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zvt_derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.58" +quote = "1.0.27" +syn = "2.0.16" diff --git a/zvt_derive/src/lib.rs b/zvt_derive/src/lib.rs new file mode 100644 index 0000000..203a58e --- /dev/null +++ b/zvt_derive/src/lib.rs @@ -0,0 +1,466 @@ +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Data, Fields}; + +#[derive(Default)] +struct ZvtBmp { + number: Option, + length_type: proc_macro2::TokenStream, + encoding_type: proc_macro2::TokenStream, +} + +impl Parse for ZvtBmp { + fn parse(s: ParseStream) -> syn::Result { + let mut number = None; + let mut length_type = quote! {zvt_builder::length::Empty }; + let mut encoding_type = quote! {zvt_builder::encoding::Default}; + loop { + let ident: syn::Ident = s.parse()?; + match &ident.to_string() as &str { + "number" => { + let _: syn::Token![=] = s.parse()?; + let value: syn::LitInt = s.parse()?; + number = Some(value.base10_parse::()?); + } + "length" => { + let _: syn::Token![=] = s.parse()?; + let e: syn::TypePath = s.parse()?; + length_type = quote! {#e}; + } + "encoding" => { + let _: syn::Token![=] = s.parse()?; + let e: syn::TypePath = s.parse()?; + encoding_type = quote! {#e}; + } + other => { + return Err(s.error(format!("Unexpected identifier: {other}"))); + } + } + if s.parse::().is_err() { + break; + } + } + Ok(ZvtBmp { + number, + length_type, + encoding_type, + }) + } +} + +#[derive(Default)] +struct ZvtTlv { + tag: Option, + encoding_type: proc_macro2::TokenStream, +} + +impl Parse for ZvtTlv { + fn parse(s: ParseStream) -> syn::Result { + let mut tag = None; + let mut encoding_type = quote! {zvt_builder::encoding::Default}; + loop { + let ident: syn::Ident = s.parse()?; + match &ident.to_string() as &str { + "tag" => { + let _: syn::Token![=] = s.parse()?; + let value: syn::LitInt = s.parse()?; + tag = Some(value.base10_parse::()?); + } + "encoding" => { + let _: syn::Token![=] = s.parse()?; + let e: syn::TypePath = s.parse()?; + encoding_type = quote! {#e}; + } + other => { + return Err(s.error(format!("Unexpected identifier: {other}"))); + } + } + if s.is_empty() { + break; + } + s.parse::()?; + } + Ok(ZvtTlv { tag, encoding_type }) + } +} + +struct ZvtControlField { + class: u8, + instr: u8, +} + +impl Parse for ZvtControlField { + fn parse(s: ParseStream) -> syn::Result { + let mut class = None; + let mut instr = None; + loop { + let ident: syn::Ident = s.parse()?; + match &ident.to_string() as &str { + "class" => { + if class.is_some() { + return Err(s.error("Duplicated `class` identifier")); + } + let _: syn::Token![=] = s.parse()?; + let value: syn::LitInt = s.parse()?; + class = Some(value.base10_parse::()?); + } + "instr" => { + if instr.is_some() { + return Err(s.error("Duplicated `instr` identifier")); + } + let _: syn::Token![=] = s.parse()?; + let value: syn::LitInt = s.parse()?; + instr = Some(value.base10_parse::()?); + } + other => { + return Err(s.error(format!("Unexpected identifier: {other}"))); + } + } + if s.is_empty() { + break; + } + s.parse::()?; + } + let class = class.ok_or(s.error("Missing `class` identifier"))?; + let instr = instr.ok_or(s.error("Missing `instr` identifier"))?; + Ok(Self { class, instr }) + } +} + +/// Optional fields have to be de-serialized separate from the others. +fn is_optional(ty: &syn::Type) -> bool { + if let syn::Type::Path(ref type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + // Data in a container (e.x. Vec) may be omitted. In this case + // the container remains empty. + if segment.ident == "Option" || segment.ident == "Vec" { + return true; + } + } + } + false +} + +/// Serializes one field of a struct. +fn derive_serialize_field(field: &syn::Field, options: &ZvtBmp) -> proc_macro2::TokenStream { + let name = field.ident.as_ref().unwrap(); + let ty = &field.ty; + let ZvtBmp { + number, + length_type, + encoding_type, + } = options; + + let number_quote = match number { + None => quote! {None}, + Some(number) => quote! {Some(zvt_builder::Tag(#number))}, + }; + + quote! { + // The `output` and `self.#name` must be defined outside of this macro. + output.append(&mut <#ty as zvt_builder::ZvtSerializerImpl<#length_type, #encoding_type >>::serialize_tagged(&input.#name, #number_quote)); + } +} + +/// Deserializes one untagged field of a struct. +/// +/// All un-tagged fields are assumed to be defined before the first tagged field. +/// The order of the un-tagged fields must be the same as they are defined in +/// the struct. +/// +/// The generated macro is unhygienic and shall be used inside +/// [derive_deserialize]. +fn derive_deserialize_field(field: &syn::Field, options: &ZvtBmp) -> proc_macro2::TokenStream { + let name = field.ident.as_ref().unwrap(); + let ty = &field.ty; + let ZvtBmp { + number: _, + length_type, + encoding_type, + } = options; + + quote! { + // The `#name` and `bytes` must be defined outside of this macro. + let (#name, mut bytes) = <#ty as zvt_builder::ZvtSerializerImpl<#length_type, #encoding_type>>::deserialize_tagged(&bytes, None)?; + } +} + +/// Deserializes one tagged field of a struct. +/// +/// The tagged fields must come after the un-tagged fields. The order of the +/// tagged fields can to be arbitrary since we can identify the fields by +/// their tag. +/// +/// The generated macro is unhygienic and shall be used inside +/// [derive_deserialize]. +fn derive_deserialize_field_tagged( + field: &syn::Field, + options: &ZvtBmp, +) -> proc_macro2::TokenStream { + let name = field.ident.as_ref().unwrap(); + let ty = &field.ty; + let ZvtBmp { + number, + length_type, + encoding_type, + } = options; + + quote! { + // The `#name` and `bytes` must be defined outside of this macro. This + // implements a match arm. + #number => { + if ! actual_tags.insert(#number) { + // The data contains duplicate fields. + return Err(zvt_builder::ZVTError::DuplicateTag(zvt_builder::Tag(#number))); + } + // Remove the number from the required tags. + required_tags.remove(&#number); + + (#name, bytes) = <#ty as zvt_builder::ZvtSerializerImpl<#length_type, #encoding_type>>::deserialize_tagged(&bytes, Some(zvt_builder::Tag(#number)))?; + } + } +} + +fn derive_deserialize( + fields: &syn::FieldsNamed, + field_options: &[ZvtBmp], + name: &syn::Ident, +) -> proc_macro2::TokenStream { + assert_eq!(fields.named.len(), field_options.len()); + + // Split the fields in positional and optional. + let mut field_names = Vec::new(); + let mut opt_field_names = Vec::new(); + let mut opt_field_quotes = Vec::new(); + let mut opt_field_tys = Vec::new(); + let mut pos_field_quotes = Vec::new(); + let mut pos_tagged_field_names = Vec::new(); + for (f, opt) in fields.named.iter().zip(field_options) { + field_names.push(f.ident.as_ref().unwrap()); + match opt.number { + None => pos_field_quotes.push(derive_deserialize_field(f, opt)), + Some(number) => { + opt_field_quotes.push(derive_deserialize_field_tagged(f, opt)); + opt_field_names.push(f.ident.as_ref().unwrap()); + if !is_optional(&f.ty) { + pos_tagged_field_names.push(quote! {#number}); + } + opt_field_tys.push(&f.ty); + } + } + } + + quote! { + fn decode<'a>(mut bytes: &'a [u8]) -> zvt_builder::ZVTResult<(#name, &'a [u8])> { + // The untagged fields are the positional fields and we deserialize + // them first. + #(#pos_field_quotes;)* + + // We have to track two things: if we have seen a value before and + // if we have - in the end - seen all required tags (tags of fields + // which aren't Option type). + let v = [#(#pos_tagged_field_names,)*]; + let mut required_tags = std::collections::HashSet::::from(v); + let mut actual_tags = std::collections::HashSet::::new(); + + let mut curr_len = bytes.len() + 1; + #(let mut #opt_field_names = <#opt_field_tys>::default();)* + while ! bytes.is_empty() && curr_len != bytes.len() { + // Make sure to terminate if we don't make progress. + curr_len = bytes.len(); + + // Try to get the next tag. + let tag: zvt_builder::Tag = match zvt_builder::encoding::Default::decode(&bytes) { + Err(_) => break, + Ok(data) => data.0, + }; + + // Try to match our tags + match tag.0 { + #(#opt_field_quotes)* + _ => {break;} + } + + } + + // We haven't found all required tags. + if !required_tags.is_empty() { + let mut as_vec: Vec<_> = required_tags.into_iter().map(|c| zvt_builder::Tag(c)).collect(); + as_vec.sort_by_key(|t| t.0); + return Err(zvt_builder::ZVTError::MissingRequiredTags(as_vec)); + } + + Ok((#name{ + #(#field_names: #field_names),* + }, &bytes)) + } + } +} + +fn derive_serialize( + fields: &syn::FieldsNamed, + field_options: &[ZvtBmp], + name: &syn::Ident, +) -> proc_macro2::TokenStream { + let field_tokens = fields + .named + .iter() + .zip(field_options.iter()) + .map(|(name, option)| derive_serialize_field(name, option)); + + quote! { + fn encode(input: &#name) -> Vec { + let mut output = Vec::new(); + #(#field_tokens)* + output + } + } +} + +fn derive_zvt_command_trait( + ast: &syn::DeriveInput, + struct_options: &Option, +) -> proc_macro2::TokenStream { + let name = &ast.ident; + match struct_options { + None => quote! { + impl zvt_builder::ZvtSerializer for # name {} + }, + Some(opts) => { + let generics = &ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let class = opts.class; + let instr = opts.instr; + quote! { + impl #impl_generics zvt_builder::ZvtCommand for #name #ty_generics #where_clause{ + const CLASS: u8 = #class; + const INSTR: u8 = #instr; + } + } + } + } +} + +fn derive(ast: &syn::DeriveInput) -> proc_macro::TokenStream { + // TODO(sirver): These should return compile_error! instead of panic. + // Check the input + let Data::Struct(ref s) = ast.data else { + panic!("Only structs are supported"); + }; + + let Fields::Named(ref fields) = s.fields else { + panic!("Only named structs are supported"); + }; + + let name = &ast.ident; + + // Get the struct-options (the outer attributes). + let mut struct_options = None; + for attr in &ast.attrs { + if attr.path().is_ident("zvt_control_field") { + if struct_options.is_some() { + panic!("Duplicated `zvt_control_field` tag.") + } + let syn::Meta::List(meta) = &attr.meta else { + panic!("We only support List attributes"); + }; + struct_options = + Some(syn::parse::(meta.tokens.clone().into()).unwrap()); + } + } + + // Get the field options (the inner attributes). + let mut field_options = Vec::new(); + for f in &fields.named { + let options = match f.attrs.len() { + 0 => ZvtBmp { + number: None, + length_type: quote! {zvt_builder::length::Empty}, + encoding_type: quote! {zvt_builder::encoding::Default}, + }, + 1 => { + let attr = &f.attrs[0]; + let name = attr.path().get_ident().unwrap().to_string(); + let syn::Meta::List(meta) = &attr.meta else { + panic!("We only support List attributes"); + }; + match name.as_str() { + "zvt_bmp" => syn::parse(meta.tokens.clone().into()).unwrap(), + "zvt_tlv" => { + let tlv = syn::parse::(meta.tokens.clone().into()).unwrap(); + ZvtBmp { + number: tlv.tag, + length_type: quote! {zvt_builder::length::Tlv}, + encoding_type: tlv.encoding_type, + } + } + _ => panic!("Unsupported tag {}", name), + } + } + _ => panic!("Zvt supports only one attribute."), + }; + field_options.push(options); + } + + let zvt_serialize = derive_serialize(fields, &field_options, name); + let zvt_deserialize = derive_deserialize(fields, &field_options, name); + let zvt_command = derive_zvt_command_trait(ast, &struct_options); + + let gen = quote! { + impl zvt_builder::encoding::Encoding<#name> for zvt_builder::encoding::Default{ + #zvt_serialize + #zvt_deserialize + } + + impl > zvt_builder::ZvtSerializerImpl for #name {} + + #zvt_command + }; + gen.into() +} + +#[proc_macro_derive(Zvt, attributes(zvt_bmp, zvt_tlv, zvt_control_field))] +pub fn parser(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse(input).unwrap(); + // Build the trait implementation + derive(&ast) +} + +#[proc_macro_derive(ZvtEnum)] +pub fn zvt_enum(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + let Data::Enum(ref s) = ast.data else { + panic!("Only enums are supported - it's in the name"); + }; + let mut variants = Vec::new(); + for variant in &s.variants { + let Fields::Unnamed(field) = &variant.fields else { + panic!("We need unnamed fields"); + }; + if field.unnamed.len() != 1 { + panic!("We need only one element"); + } + let name = &variant.ident; + let ty = &field.unnamed[0].ty; + variants.push(quote!{ + (<#ty as zvt_builder::ZvtCommand>::CLASS, <#ty as zvt_builder::ZvtCommand>::INSTR) => { + return Ok(Self::#name(<#ty as zvt_builder::ZvtSerializer>::zvt_deserialize(&bytes)?.0)); + } + }); + } + let name = ast.ident; + quote! { + impl zvt_builder::ZvtParser for #name { + fn zvt_parse(bytes: &[u8]) -> zvt_builder::ZVTResult { + if bytes.len() < 2 { + return Err(zvt_builder::ZVTError::IncompleteData); + } + match (bytes[0], bytes[1]) { + #(#variants,)* + _ => return Err(zvt_builder::ZVTError::WrongTag(zvt_builder::Tag(0))) + } + } + } + } + .into() +}