diff --git a/Cargo.lock b/Cargo.lock index 6367578..9a71682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,59 +42,42 @@ dependencies = [ ] [[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" +name = "anyhow" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] -name = "anstyle-parse" -version = "0.2.2" +name = "argh" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" dependencies = [ - "utf8parse", + "argh_derive", + "argh_shared", ] [[package]] -name = "anstyle-query" -version = "1.0.0" +name = "argh_derive" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" dependencies = [ - "windows-sys", + "argh_shared", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "anstyle-wincon" -version = "3.0.1" +name = "argh_shared" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" dependencies = [ - "anstyle", - "windows-sys", + "serde", ] -[[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" @@ -182,55 +165,9 @@ dependencies = [ "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", + "windows-targets 0.48.5", ] -[[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" @@ -252,12 +189,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -355,12 +292,6 @@ 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" @@ -410,7 +341,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -436,9 +367,9 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" @@ -463,13 +394,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -579,15 +510,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -643,15 +574,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] -[[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" @@ -706,7 +631,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -737,12 +662,6 @@ 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" @@ -840,7 +759,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -849,7 +768,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -858,13 +786,28 @@ 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", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -873,42 +816,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "yore" version = "1.1.0" @@ -925,17 +910,11 @@ dependencies = [ "anyhow", "async-stream", "chrono", - "clap", - "env_logger", "futures", - "hex", "log", "pretty-hex", - "serde", - "serde_json", "tokio", "tokio-stream", - "yore", "zvt_builder", "zvt_derive", ] @@ -952,6 +931,21 @@ dependencies = [ "zvt_derive", ] +[[package]] +name = "zvt_cli" +version = "0.1.1" +dependencies = [ + "anyhow", + "argh", + "env_logger", + "log", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "zvt", +] + [[package]] name = "zvt_derive" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 35e8b82..440badc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "zvt", "zvt_builder", + "zvt_cli", "zvt_derive", ] diff --git a/WORKSPACE b/WORKSPACE index 5e07f5a..a273a56 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -22,6 +22,7 @@ crates_repository( "//:Cargo.toml", "//zvt:Cargo.toml", "//zvt_builder:Cargo.toml", + "//zvt_cli:Cargo.toml", "//zvt_derive:Cargo.toml", ], ) diff --git a/zvt/BUILD.bazel b/zvt/BUILD.bazel index 4296f5c..bb1db49 100644 --- a/zvt/BUILD.bazel +++ b/zvt/BUILD.bazel @@ -3,10 +3,7 @@ load("@crate_index//:defs.bzl", "all_crate_deps") rust_library( name = "zvt", - srcs = glob( - ["src/**/*.rs"], - exclude = ["src/main.rs"], - ), + srcs = glob(["src/**/*.rs"]), crate_name = "zvt", edition = "2021", proc_macro_deps = all_crate_deps(proc_macro = True) + ["//zvt_derive"], @@ -14,20 +11,6 @@ rust_library( deps = all_crate_deps() + ["//zvt_builder"], ) -rust_binary( - name = "status", - srcs = glob(["src/bin/status/main.rs"]), - edition = "2021", - deps = all_crate_deps() + [":zvt"], -) - -rust_binary( - name = "feig_update", - srcs = glob(["src/bin/feig_update/main.rs"]), - edition = "2021", - deps = all_crate_deps() + [":zvt"], -) - rust_test( name = "zvt_test", srcs = [], diff --git a/zvt/Cargo.toml b/zvt/Cargo.toml index 5e9774a..8312706 100644 --- a/zvt/Cargo.toml +++ b/zvt/Cargo.toml @@ -13,18 +13,12 @@ A crate to interact with payment terminals (ECRs) that use the ZVT protocol, inc [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", "rt-multi-thread", "macros"] } async-stream = "0.3.5" -serde = { version = "1.0.185", features = ["derive"] } -serde_json = "1.0.105" +chrono = "0.4.24" futures = "0.3.28" +log = "0.4.19" pretty-hex = "0.4.0" +tokio = { version = "1.29.1", features = ["net", "io-util", "rt-multi-thread", "macros"] } +tokio-stream = "0.1.14" +zvt_builder = { version = "0.1.0", path = "../zvt_builder" } +zvt_derive = { version = "0.1.0", path = "../zvt_derive" } diff --git a/zvt/src/bin/status/main.rs b/zvt/src/bin/status/main.rs deleted file mode 100644 index ee8bff7..0000000 --- a/zvt/src/bin/status/main.rs +++ /dev/null @@ -1,157 +0,0 @@ -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/feig/packets/mod.rs b/zvt/src/feig/packets/mod.rs index 2575556..16aa314 100644 --- a/zvt/src/feig/packets/mod.rs +++ b/zvt/src/feig/packets/mod.rs @@ -65,10 +65,16 @@ pub struct WriteFile { #[derive(Debug, PartialEq, Zvt)] #[zvt_control_field(class = 0x0f, instr = 0xa1)] pub struct CVendFunctions { + #[zvt_bmp(length = length::Fixed<3>, encoding = encoding::Bcd)] + pub password: Option, + #[zvt_bmp( encoding = encoding::BigEndian)] pub instr: u16, } +pub const CVEND_FUNCTIONS_ENHANCED_SYSTEMS_INFO: u16 = 1; +pub const CVEND_FUNCTIONS_ENHANCED_FACTORY_RESET: u16 = 0x0255; + #[derive(Debug, PartialEq, Zvt)] #[zvt_control_field(class = 0x80, instr = 0x00)] pub struct WriteData { @@ -192,7 +198,10 @@ mod test { #[test] fn test_cvend_functions() { let bytes = get_bytes("1680761818.690979000_ecr_pt.blob"); - let expected = CVendFunctions { instr: 0x01 }; + let expected = CVendFunctions { + password: None, + instr: 0x01, + }; assert_eq!(CVendFunctions::zvt_deserialize(&bytes).unwrap().0, expected); assert_eq!(bytes, expected.zvt_serialize()); } diff --git a/zvt/src/feig/sequences.rs b/zvt/src/feig/sequences.rs index c0ed153..131b704 100644 --- a/zvt/src/feig/sequences.rs +++ b/zvt/src/feig/sequences.rs @@ -203,3 +203,15 @@ impl WriteFile { Box::pin(s) } } + +pub struct FactoryReset; + +#[derive(Debug, ZvtEnum)] +pub enum FactoryResetResponse { + CompletionData(packets::CompletionData), +} + +impl Sequence for FactoryReset { + type Input = super::packets::CVendFunctions; + type Output = FactoryResetResponse; +} diff --git a/zvt/src/packets.rs b/zvt/src/packets.rs index e92d087..d25461c 100644 --- a/zvt/src/packets.rs +++ b/zvt/src/packets.rs @@ -351,6 +351,16 @@ pub struct Diagnosis { pub tlv: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum DiagnosisType { + Line = 1, + Extended = 2, + Configuration = 3, + EmvConfiguration = 4, + Ep2Configuration = 5, +} + #[derive(Debug, PartialEq, Zvt)] #[zvt_control_field(class = 0x06, instr = 0x93)] pub struct Initialization { diff --git a/zvt/src/sequences.rs b/zvt/src/sequences.rs index cc43596..674e13e 100644 --- a/zvt/src/sequences.rs +++ b/zvt/src/sequences.rs @@ -3,7 +3,7 @@ use crate::{encoding, ZvtEnum, ZvtParser, ZvtSerializer}; use anyhow::Result; use async_stream::try_stream; use futures::Stream; -use log::{log_enabled, debug, Level::Debug}; +use log::{debug, log_enabled, Level::Debug}; use std::boxed::Box; use std::marker::Unpin; use std::pin::Pin; diff --git a/zvt_cli/BUILD.bazel b/zvt_cli/BUILD.bazel new file mode 100644 index 0000000..add2272 --- /dev/null +++ b/zvt_cli/BUILD.bazel @@ -0,0 +1,16 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@crate_index//:defs.bzl", "all_crate_deps") + +rust_binary( + name = "zvt_cli", + srcs = glob(["src/main.rs"]), + edition = "2021", + deps = all_crate_deps() + ["//zvt"], +) + +rust_binary( + name = "feig_update", + srcs = glob(["src/bin/feig_update/main.rs"]), + edition = "2021", + deps = all_crate_deps() + ["//zvt"], +) diff --git a/zvt_cli/Cargo.toml b/zvt_cli/Cargo.toml new file mode 100644 index 0000000..0853868 --- /dev/null +++ b/zvt_cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "zvt_cli" +edition = "2021" +authors.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +description = """ +A crate to interact with payment terminals (ECRs) that use the ZVT protocol, including stand alone commandline tools to interact with the devices. +""" + +[dependencies] +anyhow = "1.0.70" +argh = "0.1.12" +env_logger = "0.10.0" +log = "0.4.19" +serde = { version = "1.0.185", features = ["derive"] } +serde_json = "1.0.105" +tokio = { version = "1.29.1", features = ["net", "io-util", "rt-multi-thread", "macros"] } +tokio-stream = "0.1.14" +zvt = { version = "0.1.0", path = "../zvt" } diff --git a/zvt/src/bin/feig_update/main.rs b/zvt_cli/src/bin/feig_update/main.rs similarity index 84% rename from zvt/src/bin/feig_update/main.rs rename to zvt_cli/src/bin/feig_update/main.rs index aaf8e6d..ff5e201 100644 --- a/zvt/src/bin/feig_update/main.rs +++ b/zvt_cli/src/bin/feig_update/main.rs @@ -1,34 +1,35 @@ use anyhow::Result; -use clap::Parser; +use argh::FromArgs; use serde::Deserialize; use std::fs::read_to_string; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tokio::net::TcpStream; use tokio_stream::StreamExt; use zvt::{feig, packets, sequences, sequences::Sequence}; +#[derive(FromArgs, Debug)] /// Updates a feig terminal. -#[derive(Parser)] struct Args { - /// The ip and port of the payment terminal. - #[clap(long, default_value = "localhost:22000")] + /// ip and port of the payment terminal. + #[argh(option, default = "\"localhost:22000\".to_string()")] ip_address: String, - /// The password of the payment terminal. The password is a 6-digits code, + /// password of the payment terminal. The password is a 6-digits code, /// e.x. 123456. - #[clap(long)] + #[argh(option)] password: usize, - /// The config byte for the registration. - #[clap(long, default_value = "222")] + /// config byte for the registration. + #[argh(option, default = "222")] config_byte: u8, - /// Force the update. The update will otherwise be skipped if the returned + /// forces 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")] + #[argh(switch)] force: bool, - /// The folder containing the payload, e.x. firmware and app1 folders. + /// folder containing the payload, e.x. firmware and app1 folders. + #[argh(positional)] payload_dir: PathBuf, } @@ -41,16 +42,16 @@ struct UpdateSpec { /// /// 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 { +fn get_desired_version(payload_dir: &Path) -> Result { let path = payload_dir.join("app1/update.spec"); - let update_spec_str = read_to_string(&path)?; + 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(); + let args: Args = argh::from_env(); // Connect to the payment terminal. let mut socket = TcpStream::connect(&args.ip_address).await?; @@ -77,7 +78,10 @@ async fn main() -> Result<()> { { // Check the current version of the software - let request = feig::packets::CVendFunctions { instr: 1 }; + let request = feig::packets::CVendFunctions { + password: None, + 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 { diff --git a/zvt_cli/src/main.rs b/zvt_cli/src/main.rs new file mode 100644 index 0000000..131696d --- /dev/null +++ b/zvt_cli/src/main.rs @@ -0,0 +1,500 @@ +use anyhow::{bail, Result}; +use argh::FromArgs; +use env_logger::{Builder, Env}; +use std::io::Write; +use tokio::net::TcpStream; +use tokio_stream::StreamExt; +use zvt::sequences::Sequence; +use zvt::{feig, packets, sequences}; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommands { + Status(StatusArgs), + FactoryReset(FactoryResetArgs), + Registration(RegistrationArgs), + SetTerminalId(SetTerminalIdArgs), + Initialization(InitializationArgs), + Diagnosis(DiagnosisArgs), + PrintSystemConfiguration(PrintSystemConfigurationArgs), + EndOfDay(EndOfDayArgs), + ReadCard(ReadCardArgs), + Reservation(ReservationArgs), + PartialReversal(PartialReversalArgs), +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Query the cVEND status from the terminal and print to stdout. +#[argh(subcommand, name = "status")] +struct StatusArgs {} + +#[derive(FromArgs, PartialEq, Debug)] +/// Factory resets the terminal. +#[argh(subcommand, name = "factory_reset")] +struct FactoryResetArgs {} + +#[derive(FromArgs, PartialEq, Debug)] +/// Runs registration. +#[argh(subcommand, name = "registration")] +struct RegistrationArgs { + /// currency code + #[argh(option, default = "978")] + currency_code: usize, + + /// config byte. Defaults to 0xDE (= 222). + #[argh(option, default = "222")] + config_byte: u8, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Sets the terminal id. +#[argh(subcommand, name = "set_terminal_id")] +struct SetTerminalIdArgs { + /// terminal id to be set. + #[argh(option)] + terminal_id: usize, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Runs initialization. +#[argh(subcommand, name = "init")] +struct InitializationArgs {} + +fn parse_diagnosis(s: &str) -> std::result::Result { + match s { + "1" | "line" => Ok(packets::DiagnosisType::Line), + "2" | "extended" => Ok(packets::DiagnosisType::Extended), + "3" | "configuration" => Ok(packets::DiagnosisType::Configuration), + "4" | "emv_configuration" => Ok(packets::DiagnosisType::EmvConfiguration), + "5" | "ep2_configuration" => Ok(packets::DiagnosisType::Ep2Configuration), + _ => Err(format!("Invalid argument: '{s}'. Valid is line, extended, configuration, emv_configuration or ep2_configuration.")), + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Runs diagnosis +#[argh(subcommand, name = "diagnosis")] +struct DiagnosisArgs { + /// the type of diagnosis to run. See DiagnosisType for options. + #[argh(positional, from_str_fn(parse_diagnosis))] + diagnosis: packets::DiagnosisType, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Prints the terminals configuration. +#[argh(subcommand, name = "print_system_diagnosis")] +struct PrintSystemConfigurationArgs {} + +#[derive(FromArgs, PartialEq, Debug)] +/// Runs an end of day job. +#[argh(subcommand, name = "end_of_day")] +struct EndOfDayArgs {} + +#[derive(FromArgs, PartialEq, Debug)] +/// Waits for a card to be presented and prints information about the card. +#[argh(subcommand, name = "read_card")] +struct ReadCardArgs { + /// the timeout to wait for cards in seconds. + #[argh(option, default = "15")] + timeout: u8, + + /// card type as defined in Table 6. Default is chip-card. + #[argh(option, default = "16")] + card_type: u8, + + /// reading control. See Tlv tag 0x1f15 for the documentation. The default detects bank cards + /// and RFID cards, but does not send commands for payment cards. + // TODO(hrapp): If we only want to read a card once, we probably have to do something here. + #[argh(option, default = "208")] + short_card_reading_control: u8, + + /// dialog control as defined in Table 7. Which only mention the choice between 1 and 0. + /// However, some terminals accept 2 and this silences their beeps.. + #[argh(option, default = "2")] + dialog_control: u8, + + /// allowed cards ot read. The default reads all possible cards. See Tlv tag 0x1f60 for + /// documentation. + #[argh(option, default = "7")] + allowed_cards: u8, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Reserve money on a card. +#[argh(subcommand, name = "reservation")] +struct ReservationArgs { + /// currency code. The default is EUR. + #[argh(option, default = "978")] + currency_code: usize, + + /// amount in the fractional monetary unit (see + /// https://www.thefreedictionary.com/fractional+monetary+unit) which should be pre-authorized. + #[argh(option, default = "5")] + amount: usize, + + /// the payment type, defined in table 4. The default is "Payment according to PTs decision + /// excluding `GeldKarte`. + // TODO(hrapp): When bit 2 is set here, the PT should execute the payment using the data from + // the previous ReadCard command. Which might mean we do not need to reread. + #[argh(option, default = "64")] + payment_type: u8, + + /// bmp_prefix. If this is set, bmp_data must be set too. + #[argh(option)] + bmp_prefix: Option, + + /// bmp_data. If this is set, bmp_prefix must be set too. + #[argh(option)] + bmp_data: Option, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Returns some money of a prior Reservation. +#[argh(subcommand, name = "partial_reversal")] +struct PartialReversalArgs { + /// the receipt number to partially reverse. + #[argh(option)] + receipt: usize, + + /// currency code. The default is EUR. + #[argh(option, default = "978")] + currency_code: usize, + + /// the amount in the fractional monetary unit that should be reversed pre-authorized. So if + /// the original reservation was for 500 and a reversal is executed with 50, the booked amount + /// becomes 450. + #[argh(option, default = "5")] + amount: usize, + + /// see reservation. + #[argh(option, default = "64")] + payment_type: u8, + + /// see reservation. + #[argh(option)] + bmp_prefix: Option, + + /// see reservation. + #[argh(option)] + bmp_data: Option, +} + +#[derive(FromArgs, Debug)] +/// Example tool to interact with the payment terminal. +struct Args { + /// ip and port of the payment terminal. + #[argh(option, default = "\"localhost:22000\".to_string()")] + ip: String, + + /// password of the payment terminal. + #[argh(option, default = "123456")] + password: usize, + + #[argh(subcommand)] + command: SubCommands, +} + +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.args() + ) + }) + .init(); +} + +async fn status(socket: &mut TcpStream) -> Result<()> { + // Check the current version of the software + let request = feig::packets::CVendFunctions { + password: None, + instr: feig::packets::CVEND_FUNCTIONS_ENHANCED_SYSTEMS_INFO, + }; + let mut stream = feig::sequences::GetSystemInfo::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use feig::sequences::GetSystemInfoResponse::*; + match response? { + CVendFunctionsEnhancedSystemInformationCompletion(data) => log::info!("{data:#?}"), + Abort(_) => bail!("Failed to get system info. Received Abort."), + } + } + Ok(()) +} + +async fn factory_reset(socket: &mut TcpStream, password: usize) -> Result<()> { + let request = feig::packets::CVendFunctions { + password: Some(password), + instr: feig::packets::CVEND_FUNCTIONS_ENHANCED_FACTORY_RESET, + }; + let mut stream = feig::sequences::FactoryReset::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use feig::sequences::FactoryResetResponse::*; + match response? { + CompletionData(data) => log::info!("{data:#?}"), + } + } + Ok(()) +} + +async fn registration( + socket: &mut TcpStream, + password: usize, + args: &RegistrationArgs, +) -> Result<()> { + let request = packets::Registration { + password, + config_byte: args.config_byte, + currency: Some(args.currency_code), + tlv: None, + }; + + let mut stream = sequences::Registration::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::RegistrationResponse::*; + match response? { + CompletionData(data) => log::info!("{data:#?}"), + } + } + Ok(()) +} + +async fn set_terminal_id( + socket: &mut TcpStream, + password: usize, + args: &SetTerminalIdArgs, +) -> Result<()> { + let request = packets::SetTerminalId { + password, + terminal_id: Some(args.terminal_id), + }; + + let mut stream = sequences::SetTerminalId::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::SetTerminalIdResponse::*; + match response? { + CompletionData(data) => log::info!("{data:#?}"), + Abort(_) => bail!("Failed to get system info. Received Abort."), + } + } + Ok(()) +} + +async fn initialization(socket: &mut TcpStream, password: usize) -> Result<()> { + let request = packets::Initialization { password }; + + let mut stream = sequences::Initialization::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::InitializationResponse::*; + match response? { + IntermediateStatusInformation(data) => log::info!("{data:#?}"), + PrintLine(data) => log::info!("{data:#?}"), + PrintTextBlock(data) => log::info!("{data:#?}"), + CompletionData(data) => log::info!("{data:#?}"), + Abort(_) => bail!("Received Abort."), + } + } + Ok(()) +} + +async fn diagnosis(socket: &mut TcpStream, args: &DiagnosisArgs) -> Result<()> { + let request = packets::Diagnosis { + tlv: Some(packets::tlv::Diagnosis { + diagnosis_type: Some(args.diagnosis as u8), + }), + }; + + let mut stream = sequences::Diagnosis::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::DiagnosisResponse::*; + match response? { + SetTimeAndDate(data) => log::info!("{data:#?}"), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + IntermediateStatusInformation(_) | CompletionData(_) => (), + Abort(_) => bail!("Received Abort."), + } + } + Ok(()) +} + +async fn print_system_diagnosis(socket: &mut TcpStream) -> Result<()> { + let request = packets::PrintSystemConfiguration {}; + let mut stream = sequences::PrintSystemConfiguration::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::PrintSystemConfigurationResponse::*; + match response? { + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + CompletionData(_) => (), + } + } + Ok(()) +} + +async fn end_of_day(socket: &mut TcpStream, password: usize) -> Result<()> { + let request = packets::EndOfDay { password }; + let mut stream = sequences::EndOfDay::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::EndOfDayResponse::*; + match response? { + StatusInformation(data) => log::info!("{:?}", data), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + IntermediateStatusInformation(_) | CompletionData(_) => (), + Abort(data) => bail!("Received Abort: {:?}", data), + } + } + Ok(()) +} + +async fn read_card(socket: &mut TcpStream, args: &ReadCardArgs) -> Result<()> { + let request = packets::ReadCard { + timeout_sec: args.timeout, + card_type: Some(args.card_type), + dialog_control: Some(args.dialog_control), + tlv: Some(packets::tlv::ReadCard { + card_reading_control: Some(args.short_card_reading_control), + card_type: Some(args.allowed_cards), + }), + }; + + let mut stream = sequences::ReadCard::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use sequences::ReadCardResponse::*; + match response? { + IntermediateStatusInformation(_) => (), + Abort(data) => { + if data.error == zvt::constants::ErrorMessages::AbortViaTimeoutOrAbortKey as u8 { + log::info!("No card presented before timeout."); + } else { + bail!("Received Abort: {:?}", data); + } + } + StatusInformation(data) => { + log::info!("StatusInformation: {:#?}", data); + // TODO(hrapp): This is taken from internal code, but should really be in the ZVT + // library. + // Retrieve the card information. The code is mostly taken + // from python. + let tlv = data.tlv.ok_or(zvt::ZVTError::IncompleteData)?; + if !tlv.subs.is_empty() { + let subs = &tlv.subs[0]; + if subs.application_id.is_some() { + log::info!("Bank Card"); + } else { + bail!("Unknown card type") + } + } else if let Some(mut uuid) = tlv.uuid { + uuid = uuid.to_uppercase(); + if uuid.len() > 14 { + uuid = uuid[uuid.len() - 14..].to_string(); + uuid = uuid.strip_prefix("000000").unwrap_or(&uuid).to_string(); + } + log::info!("RFID: {}", uuid); + } else { + bail!(zvt::ZVTError::IncompleteData) + } + } + } + } + Ok(()) +} + +fn prep_bmp_data( + bmp_prefix: Option, + bmp_data: Option, +) -> Result> { + match (bmp_prefix, bmp_data) { + (Some(bmp_prefix), Some(bmp_data)) => Ok(Some(packets::tlv::PreAuthData { + bmp_data: Some(packets::tlv::Bmp60 { + bmp_prefix, + bmp_data, + }), + })), + (None, None) => Ok(None), + _ => bail!("Either none or both of bmp_data and bmp_prefix must be given."), + } +} +async fn reservation(socket: &mut TcpStream, args: ReservationArgs) -> Result<()> { + let tlv = prep_bmp_data(args.bmp_prefix, args.bmp_data)?; + let request = packets::Reservation { + currency: Some(args.currency_code), + amount: Some(args.amount), + payment_type: Some(args.payment_type), + tlv, + ..packets::Reservation::default() + }; + + let mut stream = sequences::Reservation::into_stream(&request, socket); + use sequences::AuthorizationResponse::*; + while let Some(response) = stream.next().await { + match response? { + IntermediateStatusInformation(_) | CompletionData(_) => (), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + Abort(data) => bail!("Received Abort: {:?}", data), + StatusInformation(data) => log::info!("StatusInformation: {:#?}", data), + } + } + Ok(()) +} + +async fn partial_reversal(socket: &mut TcpStream, args: PartialReversalArgs) -> Result<()> { + let tlv = prep_bmp_data(args.bmp_prefix, args.bmp_data)?; + + let request = packets::PartialReversal { + receipt_no: Some(args.receipt), + amount: Some(args.amount), + payment_type: Some(args.payment_type), + currency: Some(args.currency_code), + tlv, + }; + + let mut stream = sequences::PartialReversal::into_stream(&request, socket); + use sequences::PartialReversalResponse::*; + while let Some(response) = stream.next().await { + match response? { + IntermediateStatusInformation(_) | CompletionData(_) => (), + PrintLine(data) => log::info!("{}", data.text), + PrintTextBlock(data) => log::info!("{data:#?}"), + PartialReversalAbort(data) => bail!("Received Abort: {:?}", data), + StatusInformation(data) => log::info!("StatusInformation: {:#?}", data), + } + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + init_logger(); + let args: Args = argh::from_env(); + + let mut socket = TcpStream::connect(args.ip).await?; + + match args.command { + SubCommands::Status(_) => status(&mut socket).await?, + SubCommands::FactoryReset(_) => factory_reset(&mut socket, args.password).await?, + SubCommands::Registration(a) => registration(&mut socket, args.password, &a).await?, + SubCommands::SetTerminalId(a) => set_terminal_id(&mut socket, args.password, &a).await?, + SubCommands::Initialization(_) => initialization(&mut socket, args.password).await?, + SubCommands::Diagnosis(a) => diagnosis(&mut socket, &a).await?, + SubCommands::PrintSystemConfiguration(_) => print_system_diagnosis(&mut socket).await?, + SubCommands::EndOfDay(_) => end_of_day(&mut socket, args.password).await?, + SubCommands::ReadCard(a) => read_card(&mut socket, &a).await?, + SubCommands::Reservation(a) => reservation(&mut socket, a).await?, + SubCommands::PartialReversal(a) => partial_reversal(&mut socket, a).await?, + } + + Ok(()) +} diff --git a/zvt_derive/src/lib.rs b/zvt_derive/src/lib.rs index 203a58e..361270d 100644 --- a/zvt_derive/src/lib.rs +++ b/zvt_derive/src/lib.rs @@ -342,7 +342,7 @@ fn derive_zvt_command_trait( } fn derive(ast: &syn::DeriveInput) -> proc_macro::TokenStream { - // TODO(sirver): These should return compile_error! instead of panic. + // TODO(hrapp): These should return compile_error! instead of panic. // Check the input let Data::Struct(ref s) = ast.data else { panic!("Only structs are supported");