diff --git a/problem10/Cargo.lock b/problem10/Cargo.lock new file mode 100644 index 0000000..135e9a8 --- /dev/null +++ b/problem10/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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +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 = "problem10" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +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/problem10/Cargo.toml b/problem10/Cargo.toml new file mode 100644 index 0000000..7e00815 --- /dev/null +++ b/problem10/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "problem10" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1", features = ["full"] } +anyhow = "1" +itertools = "*" diff --git a/problem10/src/main.rs b/problem10/src/main.rs new file mode 100644 index 0000000..c898474 --- /dev/null +++ b/problem10/src/main.rs @@ -0,0 +1,505 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{Result, anyhow, bail}; +use itertools::Itertools; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream}, +}; + +use std::sync::Mutex; + +type Storage = HashMap; + +const PUT_USAGE: &str = "ERR usage: PUT file length newline data"; +const GET_USAGE: &str = "ERR usage: GET file [revision]"; +const LIST_USAGE: &str = "ERR usage: LIST dir"; +const INVALID_REV: &str = "invalid revision specifier"; +const NO_SUCH_FILE: &str = "no such file"; + +#[tokio::main] +async fn main() -> Result<()> { + let listener = TcpListener::bind("0.0.0.0:10000").await?; + let storage = Arc::new(Mutex::new(Storage::new())); + + loop { + let (socket, _) = listener.accept().await?; + + let this_storage = Arc::clone(&storage); + + tokio::spawn(async move { + match handle_connection(socket, this_storage).await { + Ok(()) => (), + Err(e) => { + dbg!(e); + } + } + }); + } +} + +async fn handle_connection(mut socket: TcpStream, storage: Arc>) -> Result<()> { + let (mut reader, mut writer) = socket.split(); + + let mut buffer = vec![]; + + 'outer: loop { + write_line("READY", &mut writer).await?; + + loop { + read_to_buffer(&mut reader, &mut buffer).await?; + let line = match extract_next_line(&mut buffer) { + Ok(line) => line, + Err(_) => continue, + }; + let first_word = line.split_whitespace().next().unwrap_or(""); + + dbg!(&line); + + match first_word.to_uppercase().as_str() { + "HELP" => { + write_line("OK usage: HELP|GET|PUT|LIST", &mut writer).await?; + } + + "PUT" => { + match handle_put_request(&mut writer, &mut reader, &mut buffer, &storage, line) + .await + { + Ok(()) => (), + Err(e) => { + write_line(format!("ERR {}", dbg!(e)), &mut writer).await?; + continue 'outer; + } + } + continue 'outer; + } + + "GET" => { + match handle_get_request(&mut writer, &storage, line).await { + Ok(()) => (), + Err(e) => { + write_line(format!("ERR {}", dbg!(e)), &mut writer).await?; + continue 'outer; + } + } + continue 'outer; + } + + "LIST" => { + handle_list_request(&mut writer, &storage, line).await?; + continue 'outer; + } + + s => { + write_line(format!("ERR illegal method: {}", s), &mut writer).await?; + socket.shutdown().await?; + return Ok(()); + } + }; + } + } +} + +async fn handle_list_request( + writer: &mut (impl AsyncWriteExt + Unpin), + storage: &Mutex, + line: String, +) -> Result<()> { + let mut iter = line.trim().split(' '); + iter.next(); + + match iter.next() { + Some("") => { + write_line(LIST_USAGE, writer).await?; + return Ok(()); + } + Some(dir) => { + if iter.next().is_some() { + write_line(LIST_USAGE, writer).await?; + return Ok(()); + } + if !is_legal_dirname(dir)? { + write_line("ERR illegal dir name", writer).await?; + return Ok(()); + } + let mut path = PathBuf::from(dir); + if path.is_file() { + path.push("/"); + } + + let files = get_listing(storage, path.as_path()); + let mut to_send = format!("OK {}\n", files.len()); + for entry in files { + match entry { + Entry::File(file) => { + to_send.push_str(file.path.file_name().unwrap().to_str().unwrap()); + to_send.push_str(" r"); + to_send.push_str(&file.revision.to_string()); + to_send.push('\n'); + } + Entry::Dir(path) => { + to_send.push_str(path.file_name().unwrap().to_str().unwrap()); + to_send.push_str("/ DIR\n"); + } + } + } + write_line(dbg!(to_send), writer).await?; + } + None => { + write_line(LIST_USAGE, writer).await?; + return Ok(()); + } + } + Ok(()) +} + +fn get_listing(storage: &Mutex, dir_to_search: &Path) -> Vec { + let store = storage.lock().unwrap(); + + let mut files: Vec<_> = store + .keys() + .filter(|k| k.path.parent() == Some(dir_to_search)) + .cloned() + .collect(); + + files.sort(); // so that unique_by retains the last revision + + let files: Vec<_> = files + .into_iter() + .rev() + .unique_by(|f| f.path.clone()) + .collect(); + + let dirs: Vec = store + .keys() + .filter_map(|k| { + let rel = k.path.strip_prefix(dir_to_search).ok()?; + let mut comps = rel.components(); + let first = comps.next()?; + // if there's more than one component left, `first` is a directory, + // not the file itself + comps.next().map(|_| PathBuf::from(first.as_os_str())) + }) + .unique() + .collect(); + + let mut result = vec![]; + + for file in files { + result.push(Entry::File(file)); + } + for dir in dirs { + result.push(Entry::Dir(dir)); + } + + result.sort(); + + result +} + +async fn handle_get_request( + writer: &mut (impl AsyncWriteExt + Unpin), + storage: &Mutex, + line: String, +) -> Result<()> { + let mut iter = line.trim().split(' '); + iter.next(); + + match iter.next() { + Some("") => { + write_line(GET_USAGE, writer).await?; + return Ok(()); + } + Some(filename) => match iter.next() { + Some("") => { + write_line(GET_USAGE, writer).await?; + return Ok(()); + } + Some(r) => { + if !is_legal_filename(filename)? { + write_line("ERR illegal file name", writer).await?; + return Ok(()); + } + let revision = parse_revision(r)?; + let content = get_from_storage(storage, filename, Some(revision))?; + write_line(format!("OK {}\n{}", content.len(), content), writer).await?; + } + None => { + if !is_legal_filename(filename)? { + write_line("ERR illegal file name", writer).await?; + return Ok(()); + } + let content = get_from_storage(storage, filename, None)?; + write_line(format!("OK {}\n{}", content.len(), content), writer).await?; + return Ok(()); + } + }, + None => { + write_line(GET_USAGE, writer).await?; + return Ok(()); + } + }; + Ok(()) +} + +fn get_from_storage( + storage: &Mutex, + filename: &str, + revision: Option, +) -> Result { + let path = PathBuf::from(filename); + let store = storage.lock().unwrap(); + + match revision { + Some(rev) => { + let mut files: Vec<_> = store.keys().filter(|file| file.path == path).collect(); + files.sort_by_key(|f| f.revision); + + if files.is_empty() { + return Err(anyhow!(NO_SUCH_FILE)); + } + + match files.iter().find(|f| f.revision == rev) { + Some(file) => { + let content = store.get(*file).unwrap().to_owned(); + Ok(content) + } + None => Err(anyhow!("no such revision")), + } + } + None => { + let mut files: Vec<_> = store.keys().filter(|file| file.path == path).collect(); + files.sort_by_key(|f| f.revision); + + let file = match files.last() { + Some(f) => f, + None => { + return Err(anyhow!("no such file")); + } + }; + + let content = store.get(*file).unwrap().to_owned(); + Ok(content) + } + } +} + +fn parse_revision(r: &str) -> Result { + if r.chars().all(|c| c.is_numeric()) { + return Ok(r.parse()?); + } + + let mut chars = r.chars(); + let first_is_r = if let Some(s) = chars.next() + && s == 'r' + { + true + } else { + false + }; + let rest_is_numeric = chars.clone().all(|c| c.is_numeric()); + + if first_is_r && rest_is_numeric { + return Ok(chars.collect::().parse()?); + } + + Err(anyhow!(INVALID_REV)) +} + +async fn handle_put_request( + writer: &mut (impl AsyncWriteExt + Unpin), + reader: &mut (impl AsyncReadExt + Unpin), + buffer: &mut Vec, + storage: &Mutex, + line: String, +) -> Result<()> { + let mut iter = line.trim().split(' '); + iter.next(); // skip the PUT + + match iter.next() { + Some("") => { + write_line(PUT_USAGE, writer).await?; + return Ok(()); + } + Some(filename) => { + if !is_legal_filename(filename)? { + write_line("ERR illegal file name", writer).await?; + return Ok(()); + } + + let path = PathBuf::from(filename); + + match iter.next() { + Some("") => { + write_line(PUT_USAGE, writer).await?; + return Ok(()); + } + Some(s_length) => { + // iterator must be exhausted or we have extra arguments, which are not allowed + if iter.next().is_some() { + write_line(PUT_USAGE, writer).await?; + return Ok(()); + } + + let length: u64 = s_length.parse().unwrap_or(0); + let contents = read_file_contents(length, reader, buffer).await?; + if !contents.is_ascii() { + write_line("ERR text files only", writer).await?; + return Ok(()); + } + let mut file = File::new(path); + { + let mut store = storage.lock().unwrap(); + while store.contains_key(&file) && store.get(&file).unwrap() != &contents { + file.revision += 1; + } + + store.insert(file.clone(), contents); + } + write_line(format!("OK r{}", file.revision), writer).await?; + } + None => { + write_line(PUT_USAGE, writer).await?; + return Ok(()); + } + } + } + None => { + write_line(PUT_USAGE, writer).await?; + } + } + Ok(()) +} + +async fn read_file_contents( + to_read: u64, + reader: &mut (impl AsyncReadExt + Unpin), + buffer: &mut Vec, +) -> Result { + let mut result = String::new(); + + let mut consumed = 0; + while consumed < to_read { + if buffer.is_empty() { + read_to_buffer(reader, buffer).await?; + } + + let limit = buffer.len().min((to_read - consumed) as usize); + let bytes: Vec<_> = buffer.drain(0..limit).collect(); + consumed += bytes.len() as u64; + + let s = String::from_utf8(bytes)?; + + result.push_str(&s); + } + + Ok(result) +} + +fn is_legal_dirname(dirname: &str) -> Result { + let mut iter = dirname.chars(); + + let consecutive_slashes = dirname + .chars() + .zip(dirname.chars().skip(1)) + .any(|(a, b)| a == b && a == '/'); + + Ok(dirname.starts_with('/') + && iter.all(|c| c.is_alphanumeric() || ['/', '.', '-', '_'].contains(&c)) + && !consecutive_slashes) +} + +fn is_legal_filename(filename: &str) -> Result { + let mut iter = filename.chars(); + let first_letter = iter.nth(1).ok_or(anyhow!("second char must be no /"))?; + + let consecutive_slashes = filename + .chars() + .zip(filename.chars().skip(1)) + .any(|(a, b)| a == b && a == '/'); + + Ok(filename.starts_with('/') + && !filename.ends_with('/') + && (first_letter.is_alphanumeric() || ['.', '-', '_'].contains(&first_letter)) + && iter.all(|c| c.is_alphanumeric() || ['.', '/', '-', '_'].contains(&c)) + && !consecutive_slashes) +} + +fn extract_next_line(buffer: &mut Vec) -> Result { + let pos = buffer + .iter() + .position(|b| *b == b'\n') + .ok_or_else(|| anyhow!("no newline in buffer"))?; + + let bytes: Vec<_> = buffer.drain(0..=pos).collect(); + let s = String::from_utf8(bytes)?; + + Ok(s) +} + +async fn read_to_buffer( + reader: &mut (impl AsyncReadExt + Unpin), + buffer: &mut Vec, +) -> Result { + let read_amount = reader.read_buf(buffer).await?; + if read_amount == 0 { + bail!("client disconnected"); + } + + Ok(read_amount) +} + +async fn write_line(s: S, writer: &mut W) -> Result<()> +where + S: AsRef, + W: AsyncWriteExt + Unpin, +{ + let mut s = s.as_ref().trim().to_string(); + s.push('\n'); + writer.write_all(&s.into_bytes()).await?; + + Ok(()) +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] +struct File { + path: PathBuf, + revision: u32, +} + +impl File { + fn new(path: PathBuf) -> Self { + Self { path, revision: 1 } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Entry { + File(File), + Dir(PathBuf), +} + +impl Entry { + fn sort_key(&self) -> &std::ffi::OsStr { + match self { + Entry::File(f) => f.path.file_name().unwrap_or(f.path.as_os_str()), + Entry::Dir(d) => d.as_os_str(), + } + } +} + +impl Ord for Entry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.sort_key().cmp(other.sort_key()) + } +} + +impl PartialOrd for Entry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +}