add problem 10
This commit is contained in:
Generated
+241
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "problem10"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
anyhow = "1"
|
||||||
|
itertools = "*"
|
||||||
@@ -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<File, String>;
|
||||||
|
|
||||||
|
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<Mutex<Storage>>) -> 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<Storage>,
|
||||||
|
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<Storage>, dir_to_search: &Path) -> Vec<Entry> {
|
||||||
|
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<PathBuf> = 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<Storage>,
|
||||||
|
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<Storage>,
|
||||||
|
filename: &str,
|
||||||
|
revision: Option<u32>,
|
||||||
|
) -> Result<String> {
|
||||||
|
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<u32> {
|
||||||
|
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::<String>().parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!(INVALID_REV))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_put_request(
|
||||||
|
writer: &mut (impl AsyncWriteExt + Unpin),
|
||||||
|
reader: &mut (impl AsyncReadExt + Unpin),
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
storage: &Mutex<Storage>,
|
||||||
|
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<u8>,
|
||||||
|
) -> Result<String> {
|
||||||
|
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<bool> {
|
||||||
|
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<bool> {
|
||||||
|
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<u8>) -> Result<String> {
|
||||||
|
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<u8>,
|
||||||
|
) -> Result<usize> {
|
||||||
|
let read_amount = reader.read_buf(buffer).await?;
|
||||||
|
if read_amount == 0 {
|
||||||
|
bail!("client disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(read_amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_line<S, W>(s: S, writer: &mut W) -> Result<()>
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
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<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user