diff --git a/src/dav.rs b/src/dav.rs index 8b8dae4..d89f1d4 100644 --- a/src/dav.rs +++ b/src/dav.rs @@ -185,7 +185,7 @@ pub fn multistatus(body: &[u8]) -> Result, ErrorCode> { b"getcontentlength" => { entry.length = value.trim().parse().unwrap_or(0); } - b"getetag" => entry.etag = Some(value.trim().to_owned()), + b"getetag" => entry.etag = Some(etag(&value)), _ => {} } } @@ -207,6 +207,22 @@ pub fn multistatus(body: &[u8]) -> Result, ErrorCode> { Ok(found) } +/// An entity tag in the quoted form an `ETag` header carries. +/// +/// Some servers, WsgiDAV among them, list `getetag` bare while their `ETag` +/// header quotes the same tag, so a revision read from a listing and one +/// returned by a write would differ for the same bytes. Quoting a bare tag +/// gives every revision one spelling, and `If-Match` accepts that form. A tag +/// that is already quoted, weak or strong, is kept as it is. +pub fn etag(value: &str) -> String { + let value = value.trim(); + if value.starts_with('"') || value.starts_with("W/\"") { + value.to_owned() + } else { + format!("\"{value}\"") + } +} + /// Strips any namespace prefix, so `d:href` and `D:href` read the same. fn local(name: &[u8]) -> &[u8] { match name.iter().position(|byte| *byte == b':') { @@ -264,8 +280,8 @@ pub fn removable(found: &[Found]) -> Result<(), ErrorCode> { #[cfg(test)] mod tests { use super::{ - Body, Found, basic, children, multistatus, name_of, outcome, range_outcome, - removable, url, + Body, Found, basic, children, etag, multistatus, name_of, outcome, + range_outcome, removable, url, }; use crate::sdk::ErrorCode; @@ -414,4 +430,12 @@ mod tests { assert_eq!(name_of(&found[1].href), "a b.md"); Ok(()) } + + #[test] + fn bare_etags_are_quoted_like_headers() { + assert_eq!(etag("1996000-1790356720-264"), "\"1996000-1790356720-264\""); + assert_eq!(etag(" \"v1\" "), "\"v1\""); + assert_eq!(etag("W/\"v1\""), "W/\"v1\""); + assert_eq!(etag("W/v1"), "\"W/v1\""); + } } diff --git a/src/lib.rs b/src/lib.rs index 426f1aa..daea938 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,7 +246,7 @@ impl GuestWriter for DavWriter { .headers .iter() .find(|header| header.name.eq_ignore_ascii_case("etag")) - .map(|header| header.value.clone()); + .map(|header| dav::etag(&header.value)); Ok(Some(FileInfo { kind: EntryKind::File, size: self.written.get(), @@ -369,7 +369,7 @@ impl Guest for Webdav { .headers .iter() .find(|header| header.name.eq_ignore_ascii_case("etag")) - .map(|header| header.value.clone()) + .map(|header| dav::etag(&header.value)) }); Ok(Reader::new(DavReader { call,