commit 3d9c98eecadebd7039fba3aeeaaff6124516ef55 Author: Sebastian Hugentobler Date: Sat Mar 12 16:19:54 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..8bf4d45 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/bank-server.iml b/.idea/bank-server.iml new file mode 100644 index 0000000..c254557 --- /dev/null +++ b/.idea/bank-server.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a20951 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5950b41 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,252 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bank-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "pretty_env_logger", + "thiserror", + "uuid", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "getrandom" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "libc" +version = "0.2.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5380d1f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bank-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.55" +log = "0.4.14" +pretty_env_logger = "0.4.0" +thiserror = "1.0.30" +uuid = { version = "0.8.2", features = ["v4"] } \ No newline at end of file diff --git a/src/account.rs b/src/account.rs new file mode 100644 index 0000000..ab0bcaa --- /dev/null +++ b/src/account.rs @@ -0,0 +1,142 @@ +use std::hash::{Hash, Hasher}; + +use thiserror::Error; +use anyhow::{bail, Result}; +use uuid::Uuid; +use crate::account::AccountError::{Inactive, InvalidAmount, Overdraw}; + +#[derive(Error, Debug)] +pub enum AccountError { + #[error("can not overdraw account")] + Overdraw(), + #[error("account is inactive")] + Inactive(), + #[error("amount must be > 0")] + InvalidAmount(), +} + +#[derive(Debug, Clone)] +pub struct Account { + pub number: String, + pub owner: String, + pub balance: f64, + pub is_active: bool, +} + +impl Default for Account { + fn default() -> Self { + Account { + number: Uuid::new_v4().to_string(), + owner: "".into(), + balance: 0_f64, + is_active: true, + } + } +} + +impl PartialEq for Account { + fn eq(&self, other: &Self) -> bool { + self.number == other.number + } +} + +impl Eq for Account {} + +impl Hash for Account { + fn hash(&self, state: &mut H) { + self.number.hash(state); + } +} + +impl Account { + #[cfg(test)] + pub fn new() -> Self { + Self { ..Default::default() } + } + + pub fn deposit(&mut self, amount: f64) -> Result<()> { + self.check_account(amount)?; + self.balance += amount; + Ok(()) + } + + pub fn withdraw(&mut self, amount: f64) -> Result<()> { + self.check_account(amount)?; + + if self.balance - amount < 0 as f64 { + bail!(Overdraw()); + } + self.balance -= amount; + + Ok(()) + } + + fn check_account(&self, amount: f64) -> Result<()> { + if !self.is_active { + bail!(Inactive()); + } + + if amount < 0 as f64 { + bail!(InvalidAmount()); + } + + Ok(()) + } + + pub fn passivate(&mut self) -> bool { + let is_passivated = self.balance <= 0 as f64 && self.is_active; + + if is_passivated { + self.is_active = false; + } + + is_passivated + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deposits() { + let mut acc = Account::new(); + + let ok_result = acc.deposit(10.56); + assert!(ok_result.is_ok()); + + let err_result = acc.deposit(-5.89); + assert!(err_result.is_err()); + } + + #[test] + fn withdrawals() { + let mut acc = Account::new(); + + let ok_result1 = acc.deposit(10_f64); + assert!(ok_result1.is_ok()); + + let ok_result2 = acc.withdraw(5_f64); + assert!(ok_result2.is_ok()); + + let err_result1 = acc.withdraw(10_f64); + assert!(err_result1.is_err()); + + let err_result2 = acc.withdraw(-10_f64); + assert!(err_result2.is_err()); + } + + #[test] + fn passivation() { + let mut acc = Account::new(); + let deposit_amount = 100_f64; + + acc.deposit(deposit_amount).unwrap(); + + assert!(!acc.passivate()); + acc.withdraw(deposit_amount).unwrap(); + + assert!(acc.passivate()); + assert!(!acc.passivate()); + } +} \ No newline at end of file diff --git a/src/bank.rs b/src/bank.rs new file mode 100644 index 0000000..ca38097 --- /dev/null +++ b/src/bank.rs @@ -0,0 +1,86 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::RwLock; + +#[cfg(test)] +use anyhow::Result; + +use crate::account::Account; + +pub struct Bank { + pub accounts: HashMap>, +} + +impl Bank { + pub fn new() -> Self { + Self { accounts: HashMap::new() } + } + + pub fn account_numbers(&self) -> HashSet { + self.accounts.iter() + .filter(|(_, acc)| acc.read().unwrap().is_active) + .map(|(_, acc)| acc.read().unwrap().number.clone()) + .collect() + } + + pub fn create_account(&mut self, owner: String) -> String { + let acc = Account { owner, ..Default::default() }; + let number = acc.number.clone(); + + self.accounts.insert(acc.number.clone(), RwLock::new(acc)); + + number + } + + #[cfg(test)] + pub fn transfer(&self, from: &mut Account, to: &mut Account, amount: f64) -> Result<()> { + from.withdraw(amount)?; + to.deposit(amount)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn creation() { + let mut bank = Bank::new(); + let nr = bank.create_account("Rohaja".into()); + + assert!(bank.accounts.get(&nr).is_some()); + assert!(bank.accounts.get("thisisnotanuuid").is_none()); + } + + #[test] + fn transfer() { + let mut bank = Bank::new(); + let nr1 = bank.create_account("Rohaja".into()); + let nr2 = bank.create_account("Hal".into()); + + { + let mut acc1 = bank.accounts.get(&nr1).unwrap().write().unwrap(); + let mut acc2 = bank.accounts.get(&nr2).unwrap().write().unwrap(); + + let result = bank.transfer(&mut acc1, &mut acc2, 100_f64); + assert!(result.is_err()); + + acc1.deposit(100_f64).unwrap(); + let result = bank.transfer(&mut acc1, &mut acc2, 50_f64); + assert!(result.is_ok()); + assert_eq!(acc2.balance, 50_f64); + + let result = bank.transfer(&mut acc1, &mut acc2, -50_f64); + assert!(result.is_err()); + + acc2.withdraw(50_f64).unwrap(); + assert!(acc2.passivate()); + + let result = bank.transfer(&mut acc1, &mut acc2, 50_f64); + assert!(result.is_err()); + } + + assert_eq!(1, bank.account_numbers().len()); + } +} \ No newline at end of file diff --git a/src/commands/close_account.rs b/src/commands/close_account.rs new file mode 100644 index 0000000..18c7688 --- /dev/null +++ b/src/commands/close_account.rs @@ -0,0 +1,32 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct CloseAccount; + +impl Command for CloseAccount { + fn execute(&self, bank: Arc>, data: &[u8], mut stream: &TcpStream) -> Result { + debug!("account nr bytes {:?}", data); + + let nr = String::from_utf8_lossy(data).to_string(); + info!("closing account {}...", nr); + + let bank = bank.read().unwrap(); + + let written = match bank.accounts.get(&nr) { + Some(acc) => { + let mut acc = acc.write().unwrap(); + stream.write(&protocol::account_passivate(acc.passivate()))? + } + None => stream.write(&protocol::account_passivate(false))? + }; + + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/create_account.rs b/src/commands/create_account.rs new file mode 100644 index 0000000..1b7d054 --- /dev/null +++ b/src/commands/create_account.rs @@ -0,0 +1,27 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct CreateAccount; + +impl Command for CreateAccount { + fn execute(&self, bank: Arc>, data: &[u8], mut stream: &TcpStream) -> Result { + debug!("owner nr bytes {:?}", data); + + let owner = String::from_utf8_lossy(data); + info!("creating new account with owner {}...", owner); + + let mut bank = bank.write().unwrap(); + let nr = bank.create_account(owner.into()); + info!("created account {}", nr); + + let written = stream.write(&protocol::account_nr(&nr))?; + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/deposit.rs b/src/commands/deposit.rs new file mode 100644 index 0000000..823f435 --- /dev/null +++ b/src/commands/deposit.rs @@ -0,0 +1,37 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct Deposit; + +impl Command for Deposit { + fn execute(&self, bank: Arc>, data: &[u8], mut stream: &TcpStream) -> Result { + let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?; + debug!("value bytes {:?}", value_bytes); + + let nr_bytes = &data[8..].to_vec(); + debug!("nr bytes {:?}", nr_bytes); + + let amount = f64::from_be_bytes(value_bytes); + let nr = String::from_utf8_lossy(nr_bytes).to_string(); + info!("depositing {} into {}...", amount, nr); + + let bank = bank.read().unwrap(); + + let written = match bank.accounts.get(&nr) { + Some(acc) => { + let mut acc = acc.write().unwrap(); + stream.write(&protocol::deposit(acc.deposit(amount)))? + } + None => stream.write(&protocol::error(2))? + }; + + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/fail.rs b/src/commands/fail.rs new file mode 100644 index 0000000..c23a7b4 --- /dev/null +++ b/src/commands/fail.rs @@ -0,0 +1,19 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct Fail; + +impl Command for Fail { + fn execute(&self, _: Arc>, error_code: &[u8], mut stream: &TcpStream) -> Result { + error!("sending error code {}", error_code[0]); + let written = stream.write(&protocol::error(error_code[0]))?; + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/get_account.rs b/src/commands/get_account.rs new file mode 100644 index 0000000..028131a --- /dev/null +++ b/src/commands/get_account.rs @@ -0,0 +1,32 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct GetAccount; + +impl Command for GetAccount { + fn execute(&self, bank: Arc>, data: &[u8], mut stream: &TcpStream) -> Result { + debug!("account nr bytes {:?}", data); + let nr = String::from_utf8_lossy(data).to_string(); + + info!("getting account {:?}...", nr); + + let bank = bank.read().unwrap(); + + let written = match bank.accounts.get(&nr) { + Some(acc) => { + let acc = acc.read().unwrap(); + stream.write(&protocol::account(&acc))? + } + None => stream.write(&protocol::error(2))? + }; + + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/get_account_nrs.rs b/src/commands/get_account_nrs.rs new file mode 100644 index 0000000..f28735f --- /dev/null +++ b/src/commands/get_account_nrs.rs @@ -0,0 +1,26 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct GetAccountNrs; + +impl Command for GetAccountNrs { + fn execute(&self, bank: Arc>, _: &[u8], mut stream: &TcpStream) -> Result { + info!("getting account numbers..."); + + let bank = bank.read().unwrap(); + let nrs: Vec = bank.account_numbers() + .into_iter() + .collect(); + + let written = stream.write(&protocol::account_nrs(&nrs))?; + + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..32e7188 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use crate::account::AccountError; + +use crate::bank::Bank; +use crate::commands::close_account::CloseAccount; +use crate::commands::create_account::CreateAccount; +use crate::commands::deposit::Deposit; +use crate::commands::fail::Fail; +use crate::commands::get_account::GetAccount; +use crate::commands::get_account_nrs::GetAccountNrs; +use crate::commands::pong::Pong; +use crate::commands::withdraw::Withdraw; + +mod pong; +mod fail; +mod close_account; +mod create_account; +mod deposit; +mod get_account; +mod get_account_nrs; +mod withdraw; + +pub trait Command: Sync + Send { + fn execute(&self, bank: Arc>, data: &[u8], stream: &TcpStream) -> Result; +} + +pub struct Commands { + cmds: HashMap>, +} + +impl From<&AccountError> for u8 { + fn from(error: &AccountError) -> Self { + match error { + AccountError::Overdraw() => 11, + AccountError::Inactive() => 12, + AccountError::InvalidAmount() => 13, + } + } +} + +impl Commands { + pub fn new() -> Self { + let mut cmds = HashMap::new(); + cmds.insert(1_u8, Box::new(Pong) as Box); + cmds.insert(2_u8, Box::new(CreateAccount) as Box); + cmds.insert(3_u8, Box::new(GetAccount) as Box); + cmds.insert(4_u8, Box::new(GetAccountNrs) as Box); + cmds.insert(5_u8, Box::new(CloseAccount) as Box); + cmds.insert(6_u8, Box::new(Deposit) as Box); + cmds.insert(7_u8, Box::new(Withdraw) as Box); + + Self { + cmds + } + } + + pub fn run(&self, bank: Arc>, cmd: u8, data: &[u8], stream: &TcpStream) -> Result { + match self.cmds.get(&cmd) { + None => Fail.execute(bank, &[1_u8], stream), + Some(cmd) => cmd.execute(bank, data, stream) + } + } +} diff --git a/src/commands/pong.rs b/src/commands/pong.rs new file mode 100644 index 0000000..e9d2cf6 --- /dev/null +++ b/src/commands/pong.rs @@ -0,0 +1,19 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct Pong; + +impl Command for Pong { + fn execute(&self, _: Arc>, _: &[u8], mut stream: &TcpStream) -> Result { + info!("sending 'pong'"); + let written = stream.write(&[protocol::PONG])?; + Ok(written) + } +} \ No newline at end of file diff --git a/src/commands/withdraw.rs b/src/commands/withdraw.rs new file mode 100644 index 0000000..e80164a --- /dev/null +++ b/src/commands/withdraw.rs @@ -0,0 +1,37 @@ +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; + +use crate::bank::Bank; +use crate::commands::Command; +use crate::protocol; + +pub struct Withdraw; + +impl Command for Withdraw { + fn execute(&self, bank: Arc>, data: &[u8], mut stream: &TcpStream) -> Result { + let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?; + debug!("value bytes {:?}", value_bytes); + + let nr_bytes = &data[8..].to_vec(); + debug!("nr bytes {:?}", nr_bytes); + + let amount = f64::from_be_bytes(value_bytes); + let nr = String::from_utf8_lossy(nr_bytes).to_string(); + info!("withdrawing {} from {}...", amount, nr); + + let bank = bank.read().unwrap(); + + let written = match bank.accounts.get(&nr) { + Some(acc) => { + let mut acc = acc.write().unwrap(); + stream.write(&protocol::withdraw(acc.withdraw(amount)))? + } + None => stream.write(&protocol::error(2))? + }; + + Ok(written) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..831e5b3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,17 @@ +#[macro_use] +extern crate log; + +mod account; +mod bank; +mod protocol; +mod server; +mod threadpool; +mod commands; + +fn main() { + pretty_env_logger::init(); + + let host = "127.0.0.1:1234"; + info!("running on {}", host); + server::run(host, 8).expect("failed to run server"); +} diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..d697688 --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,85 @@ +use crate::account::{Account, AccountError}; +use anyhow::Result; + +pub const START: [u8; 2] = [0xde, 0xad]; +pub const PONG: u8 = 0b0010_0000; + +fn to_error_code(error: anyhow::Error, default: u8) -> u8 { + match error.root_cause().downcast_ref::() { + Some(e) => e.into(), + None => default, + } +} + +pub fn account_nr(nr: &str) -> Vec { + let mut response = vec![PONG]; + response.append(&mut nr.as_bytes().to_vec()); + debug!("account number bytes: {:?}", response); + response +} + +pub fn account_passivate(was_passivated: bool) -> Vec { + let is_active_byte: u8 = if was_passivated { 1 } else { 0 }; + vec![PONG | is_active_byte] +} + +pub fn deposit(result: Result<()>) -> Vec { + match result { + Err(e) => error(to_error_code(e, 10)).to_vec(), + Ok(_) => vec![PONG] + } +} + +pub fn withdraw(result: Result<()>) -> Vec { + match result { + Err(e) => error(to_error_code(e, 10)).to_vec(), + Ok(_) => vec![PONG] + } +} + +pub fn account_nrs(nrs: &[String]) -> Vec { + let mut response = vec![PONG]; + + nrs.iter().for_each(|key| { + let key_bytes = key.as_bytes(); + debug!("account number bytes: {:?}", key_bytes); + + response.push(key_bytes.len() as u8); + response.append(&mut key_bytes.to_vec()); + }); + + debug!("account numbers bytes: {:?}", response); + + response +} + +pub fn account(account: &Account) -> Vec { + let is_active_byte: u8 = if account.is_active { 1 } else { 0 }; + + let mut response = vec![PONG | is_active_byte]; + + let nr_bytes = account.number.as_bytes(); + let mut nr_bytes_sized = vec![nr_bytes.len() as u8]; + nr_bytes_sized.append(&mut nr_bytes.to_vec()); + debug!("nr bytes: {:?}", nr_bytes_sized); + + let owner_bytes = account.owner.as_bytes(); + let mut owner_bytes_sized = vec![owner_bytes.len() as u8]; + owner_bytes_sized.append(&mut owner_bytes.to_vec()); + debug!("owner bytes: {:?}", owner_bytes_sized); + + let balance: [u8; 8] = account.balance.to_be_bytes(); + debug!("balance bytes: {:?}", balance); + + response.append(&mut nr_bytes_sized); + response.append(&mut owner_bytes_sized); + response.append(&mut balance.to_vec()); + + debug!("account response: {:?}", response); + + response +} + +pub fn error(code: u8) -> [u8; 1] { + [0b0100_0000 | code] +} \ No newline at end of file diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..a9758f6 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,60 @@ +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::{Arc, RwLock}; + +use anyhow::{bail, Result}; + +use crate::bank::Bank; +use crate::commands::Commands; +use crate::protocol; +use crate::threadpool::ThreadPool; + +pub fn run(host: &str, threads: usize) -> Result<()> { + let listener = TcpListener::bind(host)?; + let pool = ThreadPool::new(threads); + let cmds = Arc::new(Commands::new()); + + let bank = Arc::new(RwLock::new(Bank::new())); + + for stream in listener.incoming() { + let cmds = cmds.clone(); + let bank = bank.clone(); + + let stream = stream?; + + pool.execute(move || { + if let Err(e) = handle_connection(bank, &cmds, stream) { + error!("{}", e); + } + }); + } + + Ok(()) +} + +fn handle_connection(bank: Arc>, cmds: &Commands, mut stream: TcpStream) -> Result<()> { + const BUF_SIZE: usize = 64; + + let mut data: Vec = vec![]; + let mut buffer = [0; BUF_SIZE]; + + let mut read = stream.read(&mut buffer)?; + while read > 0 { + data.append(&mut buffer[..read].to_vec()); + read = stream.read(&mut buffer)?; + } + + if data[..2] != protocol::START { + bail!("got {:?} as first bytes, not my problem", &data[..2]); + } + + let cmd: u8 = (data[2] & 0b1111_0000) >> 4; + trace!("got command {}", cmd); + + cmds.run(bank, cmd, &data[3..], &stream)?; + + stream.flush()?; + + Ok(()) +} + diff --git a/src/threadpool.rs b/src/threadpool.rs new file mode 100644 index 0000000..22267d3 --- /dev/null +++ b/src/threadpool.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; +use std::sync::mpsc; +use std::sync::Mutex; +use std::thread; + +pub struct ThreadPool { + workers: Vec, + sender: mpsc::Sender, +} + +type Job = Box; + +enum Message { + NewJob(Job), + Terminate, +} + +impl ThreadPool { + pub fn new(size: usize) -> ThreadPool { + assert!(size > 0); + + let (sender, receiver) = mpsc::channel(); + + let receiver = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(Worker::new(id, Arc::clone(&receiver))); + } + + ThreadPool { workers, sender } + } + + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + let job = Box::new(f); + + self.sender.send(Message::NewJob(job)).unwrap(); + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + trace!("Sending terminate message to all workers."); + + for _ in &self.workers { + self.sender.send(Message::Terminate).unwrap(); + } + + trace!("Shutting down all workers."); + + for worker in &mut self.workers { + trace!("Shutting down worker {}", worker.id); + + if let Some(thread) = worker.thread.take() { + thread.join().unwrap(); + } + } + } +} + +struct Worker { + id: usize, + thread: Option>, +} + +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let message = receiver.lock().unwrap().recv().unwrap(); + + match message { + Message::NewJob(job) => { + trace!("Worker {} got a job; executing.", id); + + job(); + } + Message::Terminate => { + trace!("Worker {} was told to terminate.", id); + + break; + } + } + }); + + Worker { + id, + thread: Some(thread), + } + } +} \ No newline at end of file