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