From ff54fe9e795616505f5ae94f11e3ff90bd39285b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20Monn=C3=A9?= Date: Fri, 19 Jun 2026 15:49:18 +0200 Subject: [PATCH] fix: quote ETags per RFC 9110 in InMemory and LocalFileSystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloud-backed stores (AWS, Azure, GCP) return ETags as quoted-strings (e.g. `"abc123"`) and expect `If-Match`/`If-None-Match` values in that same form, as required by RFC 9110 ยง8.8.3. `InMemory` and `LocalFileSystem`, by contrast, were producing bare tokens, making them inconsistent with the cloud backends and harder to use as drop-in replacements. --- src/lib.rs | 16 ++++++++-------- src/local.rs | 2 +- src/memory.rs | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b2d9a644..ebe04d43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2492,7 +2492,7 @@ mod tests { location: Path::from("test"), last_modified: Utc.timestamp_nanos(100), size: 100, - e_tag: Some("123".to_string()), + e_tag: Some("\"123\"".to_string()), version: None, }; @@ -2521,16 +2521,16 @@ mod tests { options = GetOptions::default(); - options.if_match = Some("123".to_string()); + options.if_match = Some("\"123\"".to_string()); options.check_preconditions(&meta).unwrap(); - options.if_match = Some("123,354".to_string()); + options.if_match = Some("\"123\",\"354\"".to_string()); options.check_preconditions(&meta).unwrap(); - options.if_match = Some("354, 123,".to_string()); + options.if_match = Some("\"354\", \"123\"".to_string()); options.check_preconditions(&meta).unwrap(); - options.if_match = Some("354".to_string()); + options.if_match = Some("\"354\"".to_string()); options.check_preconditions(&meta).unwrap_err(); options.if_match = Some("*".to_string()); @@ -2542,16 +2542,16 @@ mod tests { options = GetOptions::default(); - options.if_none_match = Some("123".to_string()); + options.if_none_match = Some("\"123\"".to_string()); options.check_preconditions(&meta).unwrap_err(); options.if_none_match = Some("*".to_string()); options.check_preconditions(&meta).unwrap_err(); - options.if_none_match = Some("1232".to_string()); + options.if_none_match = Some("\"1232\"".to_string()); options.check_preconditions(&meta).unwrap(); - options.if_none_match = Some("23, 123".to_string()); + options.if_none_match = Some("\"23\", \"123\"".to_string()); options.check_preconditions(&meta).unwrap_err(); // If-None-Match takes precedence diff --git a/src/local.rs b/src/local.rs index 1ef18e10..289fcaf7 100644 --- a/src/local.rs +++ b/src/local.rs @@ -1418,7 +1418,7 @@ fn get_etag(metadata: &Metadata) -> String { // Use an ETag scheme based on that used by many popular HTTP servers // // - format!("{inode:x}-{mtime:x}-{size:x}") + format!("\"{inode:x}-{mtime:x}-{size:x}\"") } fn convert_metadata(metadata: Metadata, location: Path) -> ObjectMeta { diff --git a/src/memory.rs b/src/memory.rs index cbdcbca6..26489475 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -157,7 +157,7 @@ impl Storage { source: format!("Object at location {location} not found").into(), }), Some(e) => { - let existing = e.e_tag.to_string(); + let existing = format!("\"{}\"", e.e_tag); let expected = v.e_tag.ok_or(Error::MissingETag)?; if existing == expected { *e = entry; @@ -217,7 +217,7 @@ impl ObjectStore for InMemory { storage.next_etag += 1; Ok(PutResult { - e_tag: Some(etag.to_string()), + e_tag: Some(format!("\"{}\"", etag)), version: None, extensions: Default::default(), }) @@ -238,7 +238,7 @@ impl ObjectStore for InMemory { async fn get_opts(&self, location: &Path, options: GetOptions) -> Result { let entry = self.entry(location)?; - let e_tag = entry.e_tag.to_string(); + let e_tag = format!("\"{}\"", entry.e_tag); let meta = ObjectMeta { location: location.clone(), @@ -331,7 +331,7 @@ impl ObjectStore for InMemory { location: key.clone(), last_modified: value.last_modified, size: value.data.len() as u64, - e_tag: Some(value.e_tag.to_string()), + e_tag: Some(format!("\"{}\"", value.e_tag)), version: None, }) }) @@ -376,7 +376,7 @@ impl ObjectStore for InMemory { location: k.clone(), last_modified: v.last_modified, size: v.data.len() as u64, - e_tag: Some(v.e_tag.to_string()), + e_tag: Some(format!("\"{}\"", v.e_tag)), version: None, }; objects.push(object); @@ -480,7 +480,7 @@ impl MultipartStore for InMemory { } let etag = storage.insert(path, buf.into(), upload.attributes); Ok(PutResult { - e_tag: Some(etag.to_string()), + e_tag: Some(format!("\"{}\"", etag)), version: None, extensions: Default::default(), }) @@ -547,7 +547,7 @@ impl MultipartUpload for InMemoryUpload { ); Ok(PutResult { - e_tag: Some(etag.to_string()), + e_tag: Some(format!("\"{}\"", etag)), version: None, extensions: Default::default(), })