From 924964fcfb6e9863307db382a8306ebb52999f29 Mon Sep 17 00:00:00 2001 From: timeshifter Date: Mon, 13 Jul 2026 21:29:03 +0200 Subject: [PATCH] add problem 06 --- problem06/Cargo.lock | 241 ++++++++++++++++++++++ problem06/Cargo.toml | 9 + problem06/src/main.rs | 457 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 707 insertions(+) create mode 100644 problem06/Cargo.lock create mode 100644 problem06/Cargo.toml create mode 100644 problem06/src/main.rs diff --git a/problem06/Cargo.lock b/problem06/Cargo.lock new file mode 100644 index 0000000..a57fb37 --- /dev/null +++ b/problem06/Cargo.lock @@ -0,0 +1,241 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "problem06" +version = "0.1.0" +dependencies = [ + "anyhow", + "itertools", + "tokio", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/problem06/Cargo.toml b/problem06/Cargo.toml new file mode 100644 index 0000000..659cb40 --- /dev/null +++ b/problem06/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "problem06" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1", features = ["full"] } +anyhow = "1" +itertools = "0" diff --git a/problem06/src/main.rs b/problem06/src/main.rs new file mode 100644 index 0000000..c7b8789 --- /dev/null +++ b/problem06/src/main.rs @@ -0,0 +1,457 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex, MutexGuard}; + +use anyhow::{Result, ensure}; +use itertools::Itertools; +use tokio::io::AsyncWriteExt; +use tokio::sync::broadcast::{self, Receiver, Sender}; +use tokio::{ + io::AsyncReadExt, + net::{TcpListener, TcpStream}, +}; + +type PlateLog = HashMap>; +type TicketLog = HashSet<(String, usize)>; + +#[tokio::main] +async fn main() -> Result<()> { + let listener = TcpListener::bind("0.0.0.0:10000").await?; + + let plates = Arc::new(Mutex::new(HashMap::new())); + let days_with_issued_tickets = Arc::new(Mutex::new(TicketLog::new())); + let ticket_queue = Arc::new(Mutex::new(vec![])); + let (tx, _) = broadcast::channel(100_000); + + loop { + let (socket, _) = listener.accept().await?; + let this_plates = plates.clone(); + let this_tx = tx.clone(); + let this_queue = ticket_queue.clone(); + let this_days_tickets = days_with_issued_tickets.clone(); + + tokio::spawn(async move { + match handle_connection(socket, this_plates, this_queue, this_days_tickets, this_tx) + .await + { + Ok(()) => (), + Err(e) => { + dbg!(e); + } + } + }); + } +} + +async fn handle_connection( + mut socket: TcpStream, + plates: Arc>, + ticket_queue: Arc>>, + days_with_issued_tickets: Arc>, + tx: Sender, +) -> Result<()> { + let (mut reader, mut writer) = socket.split(); + + let mut heartbeat_interval = None; + let mut timer = tokio::time::interval(std::time::Duration::from_hours(100)); + timer.tick().await; + + loop { + tokio::select! { + _ = timer.tick() => { + writer.write_all(&[0x41]).await?; + } + byte = reader.read_u8() => { + match byte? { + 0x40 => { + if heartbeat_interval.is_some() { + send_error_message(&mut writer, "multiple heartbeat requests").await?; + return Ok(()); + } + let interval = reader.read_u32().await?; + heartbeat_interval = Some(interval); + if interval > 0 { + timer = tokio::time::interval(std::time::Duration::from_millis( + 100.max((interval as u64 * 100) - 100), + )); + } + } + 0x80 => { + camera_main_loop(reader, writer, plates, ticket_queue, timer, heartbeat_interval, tx).await?; + return Ok(()); + } + 0x81 => { + let rx = tx.subscribe(); + dispatcher_main_loop(reader, writer, ticket_queue, days_with_issued_tickets, timer, heartbeat_interval, rx) + .await?; + return Ok(()); + } + other => { + return send_error_message( + &mut writer, + &format!("initialized with wrong message type 0x{other:x}"), + ) + .await; + } + }; + } + } + } +} + +async fn dispatcher_main_loop( + mut reader: tokio::net::tcp::ReadHalf<'_>, + mut writer: tokio::net::tcp::WriteHalf<'_>, + ticket_queue: Arc>>, + days_with_issued_tickets: Arc>, + mut timer: tokio::time::Interval, + mut heartbeat_interval: Option, + mut rx: Receiver, +) -> Result<()> { + let num_roads = reader.read_u8().await?; + let mut roads = vec![]; + + for _ in 0..num_roads { + roads.push(reader.read_u16().await?); + } + + work_ticket_queue( + &roads, + &mut writer, + &ticket_queue, + &days_with_issued_tickets, + ) + .await?; + + loop { + tokio::select! { + _ = timer.tick() => { + writer.write_all(&[0x41]).await?; + } + _ = rx.recv() => { + work_ticket_queue(&roads, &mut writer, &ticket_queue, &days_with_issued_tickets).await?; + } + byte = reader.read_u8() => { + match byte? { + 0x40 => { + if heartbeat_interval.is_some() { + send_error_message(&mut writer, "multiple heartbeat requests").await?; + return Ok(()); + } + heartbeat_interval = Some(reader.read_u32().await?); + if let Some(interval) = heartbeat_interval + && interval > 0 + { + timer = tokio::time::interval(std::time::Duration::from_millis( + 100.max((interval * 100) as u64 - 100), + )); + } + } + _ => { + return send_error_message(&mut writer, "received wrong message type").await; + } + } + } + + } + } +} + +async fn work_ticket_queue( + roads: &[u16], + writer: &mut tokio::net::tcp::WriteHalf<'_>, + ticket_queue: &Arc>>, + days_with_issued_tickets: &Arc>, +) -> Result<()> { + let messages = generate_ticket_messages(ticket_queue, roads, days_with_issued_tickets).await?; + + for message in messages { + writer.write_all(&message).await?; + } + + Ok(()) +} + +async fn generate_ticket_messages( + queue: &Arc>>, + roads: &[u16], + days_with_issued_tickets: &Arc>, +) -> Result>> { + let mut queue = queue.lock().unwrap(); + + let mut days = days_with_issued_tickets.lock().unwrap(); + + let own_tickets = queue + .iter() + .positions(|ticket| roads.contains(&ticket.road)); + + let mut messages = vec![]; + let mut to_remove = vec![]; + + for idx in own_tickets { + let ticket = queue.get(idx).unwrap(); + to_remove.push(idx); + + let day1 = timestamp_to_day(ticket.timestamp1); + let day2 = timestamp_to_day(ticket.timestamp2); + + let key1 = (ticket.plate.clone(), day1); + let key2 = (ticket.plate.clone(), day2); + + if days.contains(&key1) || days.contains(&key2) { + continue; + } + + days.insert(key1); + days.insert(key2); + + let to_wire = ticket_to_vec_u8(ticket); + + messages.push(to_wire); + } + + to_remove.sort(); + + for idx in to_remove.iter().rev() { + queue.remove(*idx); + } + + Ok(messages) +} + +fn timestamp_to_day(timestamp: u32) -> usize { + (timestamp / 86400) as _ +} + +fn ticket_to_vec_u8(ticket: &Ticket) -> Vec { + let mut to_wire = vec![0x21]; + + // plate + to_wire.push(ticket.plate.len() as u8); + for byte in ticket.plate.as_bytes() { + to_wire.push(*byte); + } + + // road + for byte in ticket.road.to_be_bytes() { + to_wire.push(byte); + } + + // mile 1, timestamp 1 + for byte in ticket.mile1.to_be_bytes() { + to_wire.push(byte); + } + for byte in ticket.timestamp1.to_be_bytes() { + to_wire.push(byte); + } + + // mile 2, timestamp 2 + for byte in ticket.mile2.to_be_bytes() { + to_wire.push(byte); + } + for byte in ticket.timestamp2.to_be_bytes() { + to_wire.push(byte); + } + + // speed + for byte in ticket.speed.to_be_bytes() { + to_wire.push(byte); + } + + to_wire +} + +async fn camera_main_loop( + mut reader: tokio::net::tcp::ReadHalf<'_>, + mut writer: tokio::net::tcp::WriteHalf<'_>, + plates: Arc>, + ticket_queue: Arc>>, + mut timer: tokio::time::Interval, + mut heartbeat_interval: Option, + tx: Sender, +) -> Result<()> { + let myself = Camera { + road: reader.read_u16().await?, + mile: reader.read_u16().await?, + limit: reader.read_u16().await?, + }; + + loop { + tokio::select! { + _ = timer.tick() => { + writer.write_all(&[0x41]).await?; + } + byte = reader.read_u8() => { + match byte? { + + 0x20 => { + let plate = read_string(&mut reader).await?; + let timestamp = reader.read_u32().await?; + { + let mut this_plates = plates.lock().unwrap(); + this_plates + .entry(plate.clone()) + .and_modify(|v| v.push((myself.clone(), timestamp))) + .or_insert(vec![(myself.clone(), timestamp)]); + + for ticket in find_possible_tickets(this_plates, &myself, plate, timestamp) + { + ticket_queue.lock().unwrap().push(ticket); + let _ = tx.send(ChannelMessage::TicketIssued); + } + } + } + 0x40 => { + if heartbeat_interval.is_some() { + send_error_message(&mut writer, "multiple heartbeat requests").await?; + return Ok(()); + } + heartbeat_interval = Some(reader.read_u32().await?); + if let Some(interval) = heartbeat_interval + && interval > 0 + { + timer = tokio::time::interval(std::time::Duration::from_millis( + 100.max((interval * 100) as u64 - 100), + )); + } + } + + _ => { + return send_error_message(&mut writer, "received wrong message type").await; + } + } + } + } + } +} + +fn find_possible_tickets( + plates: MutexGuard<'_, PlateLog>, + this_camera: &Camera, + this_plate: String, + this_timestamp: u32, +) -> Vec { + let relevant: Vec<_> = plates + .iter() + .filter(|(plate, _)| **plate == this_plate) + .flat_map(|(_, camera_timestamp_map)| camera_timestamp_map) + .filter(|(camera, _)| camera.road == this_camera.road) + .collect(); + + let violations = relevant + .iter() + .combinations(2) + .filter(|data| { + data.iter() + .any(|(_, timestamp)| *timestamp == this_timestamp) + }) + .map(|data| (data.clone(), calculate_speed(data).unwrap())) + .filter(|(_, speed)| *speed > this_camera.limit * 100); + + let mut result = vec![]; + + for violation in violations { + let (camera_timestamp_map, speed) = violation; + let ticket = Ticket::new( + this_plate.clone(), + this_camera.road, + camera_timestamp_map[0].0.mile, + camera_timestamp_map[0].1, + camera_timestamp_map[1].0.mile, + camera_timestamp_map[1].1, + speed, + ); + + result.push(ticket); + } + + result +} + +fn calculate_speed(timestamps: Vec<&&(Camera, u32)>) -> Option { + let mut data = timestamps.clone(); + + data.sort_by_key(|(_, time)| time); + + let dt = (data[1].1 - data[0].1) as u64; + let ds = (data[1].0.mile as i64 - data[0].0.mile as i64).unsigned_abs(); + + if dt == 0 { + return None; + } + + Some(((ds * 3600 * 100) / dt) as _) +} + +async fn send_error_message( + writer: &mut tokio::net::tcp::WriteHalf<'_>, + message: &str, +) -> Result<()> { + let mut to_wire = vec![0x10]; + + ensure!(message.len() <= 255, "message too long"); + to_wire.push(message.len() as u8); + + for byte in message.as_bytes() { + to_wire.push(*byte); + } + + writer.write_all(&to_wire).await?; + Ok(()) +} + +async fn read_string(r: &mut (impl AsyncReadExt + Unpin)) -> Result { + let len = r.read_u8().await?; + let mut buf = vec![0u8; len as usize]; + r.read_exact(&mut buf).await?; + Ok(String::from_utf8(buf)?) +} + +#[derive(Clone, Debug)] +struct Camera { + road: u16, + mile: u16, + limit: u16, +} + +#[derive(Clone, Debug)] +struct Ticket { + plate: String, + road: u16, + mile1: u16, + timestamp1: u32, + mile2: u16, + timestamp2: u32, + speed: u16, +} + +impl Ticket { + fn new( + plate: String, + road: u16, + mut mile1: u16, + mut timestamp1: u32, + mut mile2: u16, + mut timestamp2: u32, + speed: u16, + ) -> Self { + if timestamp2 < timestamp1 { + (mile1, mile2) = (mile2, mile1); + (timestamp1, timestamp2) = (timestamp2, timestamp1); + } + Self { + plate, + road, + mile1, + timestamp1, + mile2, + timestamp2, + speed, + } + } +} + +#[derive(Clone, Debug)] +#[non_exhaustive] +enum ChannelMessage { + TicketIssued, +}