Bound reads to their limit, treat 416 as end of file, and refuse to remove a non-empty directory

This commit is contained in:
Danny Kim
2026-09-25 15:22:18 +09:00
parent df3635569a
commit 3d69750a26
6 changed files with 222 additions and 34 deletions
+60 -29
View File
@@ -9,7 +9,7 @@ use sdk::{
GuestReader, GuestWriter, ListPage, Locator, Reader, Setting, WriteCondition,
Writer,
};
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
/// The PROPFIND body. Asking for named properties rather than allprop keeps
/// replies small on servers that store a great many of them.
@@ -131,6 +131,20 @@ fn content_type() -> host::Header {
}
}
/// Asks the server about one path, and its children when `depth` is 1.
fn describe(
path: &str,
depth_value: &str,
) -> Result<Vec<dav::Found>, ErrorCode> {
let body = exchange(
host::Method::Propfind,
path,
vec![depth(depth_value), content_type()],
PROPS.as_bytes(),
)?;
dav::multistatus(&body)
}
fn info_of(found: &dav::Found) -> FileInfo {
FileInfo {
kind: if found.collection {
@@ -147,11 +161,17 @@ fn info_of(found: &dav::Found) -> FileInfo {
struct DavReader {
call: host::Exchange,
revision: Option<String>,
body: RefCell<dav::Body>,
}
impl GuestReader for DavReader {
fn pull(&self, limit: u32) -> Result<Vec<u8>, ErrorCode> {
self.call.pull(limit.min(MAX_CHUNK)).map_err(transport)
self
.body
.borrow_mut()
.pull(limit.min(MAX_CHUNK) as usize, |wanted| {
self.call.pull(wanted).map_err(transport)
})
}
fn revision(&self) -> Option<String> {
self.revision.clone()
@@ -162,6 +182,7 @@ impl GuestReader for DavReader {
struct DavWriter {
call: host::Exchange,
done: RefCell<bool>,
written: Cell<u64>,
}
impl GuestWriter for DavWriter {
@@ -169,7 +190,9 @@ impl GuestWriter for DavWriter {
if *self.done.borrow() {
return Err(ErrorCode::InvalidInput);
}
self.call.push(&data).map_err(transport)
self.call.push(&data).map_err(transport)?;
self.written.set(self.written.get() + data.len() as u64);
Ok(())
}
/// One call is enough: the request body is already complete, so the response
/// head settles the write. The etag it returns, when the server sends one,
@@ -188,7 +211,7 @@ impl GuestWriter for DavWriter {
.map(|header| header.value.clone());
Ok(Some(FileInfo {
kind: EntryKind::File,
size: 0,
size: self.written.get(),
revision,
}))
}
@@ -240,14 +263,7 @@ impl Guest for Webdav {
}
fn stat(target: Locator) -> Result<FileInfo, ErrorCode> {
let path = route(&target);
let body = exchange(
host::Method::Propfind,
&path,
vec![depth("0"), content_type()],
PROPS.as_bytes(),
)?;
let found = dav::multistatus(&body)?;
let found = describe(&route(&target), "0")?;
let entry = found.first().ok_or(ErrorCode::NotFound)?;
Ok(info_of(entry))
}
@@ -264,17 +280,9 @@ impl Guest for Webdav {
Some(value) => value.parse().map_err(|_| ErrorCode::InvalidInput)?,
None => 0,
};
let body = exchange(
host::Method::Propfind,
&path,
vec![depth("1"), content_type()],
PROPS.as_bytes(),
)?;
let found = dav::multistatus(&body)?;
// The first response is the collection itself, which is not its own child.
let children: Vec<_> = found
let found = describe(&path, "1")?;
let children: Vec<_> = dav::children(&found)?
.iter()
.skip(1)
.map(|entry| Entry {
name: dav::name_of(&entry.href),
info: info_of(entry),
@@ -288,8 +296,15 @@ impl Guest for Webdav {
})
}
/// A GET on a collection answers with whatever page the server likes to
/// show, so the path is checked to be a file before any byte is read.
fn open_read(target: Locator, offset: u64) -> Result<Reader, ErrorCode> {
let path = route(&target);
let found = describe(&path, "0")?;
let entry = found.first().ok_or(ErrorCode::NotFound)?;
if entry.collection {
return Err(ErrorCode::InvalidInput);
}
let call = begin(
host::Method::Get,
&path,
@@ -299,13 +314,25 @@ impl Guest for Webdav {
}],
)?;
let head = call.respond().map_err(transport)?;
dav::outcome(head.status)?;
let revision = head
.headers
.iter()
.find(|header| header.name.eq_ignore_ascii_case("etag"))
.map(|header| header.value.clone());
Ok(Reader::new(DavReader { call, revision }))
let body = if dav::range_outcome(head.status)? {
dav::Body::default()
} else {
dav::Body::empty()
};
// The listing's etag is the one stat and list report, and the strong form
// a later conditional write must send; the GET's may be a weak variant.
let revision = entry.etag.clone().or_else(|| {
head
.headers
.iter()
.find(|header| header.name.eq_ignore_ascii_case("etag"))
.map(|header| header.value.clone())
});
Ok(Reader::new(DavReader {
call,
revision,
body: RefCell::new(body),
}))
}
fn open_write(
@@ -329,6 +356,7 @@ impl Guest for Webdav {
Ok(Writer::new(DavWriter {
call,
done: RefCell::new(false),
written: Cell::new(0),
}))
}
@@ -368,8 +396,11 @@ impl Guest for Webdav {
Self::stat(Locator::Path(destination))
}
/// DELETE takes a whole collection at once, so a directory is looked at
/// first and refused unless it is empty.
fn remove(target: Locator) -> Result<(), ErrorCode> {
let path = route(&target);
dav::removable(&describe(&path, "1")?)?;
exchange(host::Method::Delete, &path, Vec::new(), &[])?;
Ok(())
}