This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git


The following commit(s) were added to refs/heads/main by this push:
     new d9eaded  fix: quote ETags per RFC 9110 in InMemory and LocalFileSystem 
(#770)
d9eaded is described below

commit d9eaded6c8440d4f87a680fdc47c6bcc7cbf23c6
Author: Sylvain Monné <[email protected]>
AuthorDate: Mon Jun 22 15:39:23 2026 +0200

    fix: quote ETags per RFC 9110 in InMemory and LocalFileSystem (#770)
    
    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 b2d9a64..ebe04d4 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 1ef18e1..289fcaf 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
     // <https://httpd.apache.org/docs/2.2/mod/core.html#fileetag>
     // 
<https://stackoverflow.com/questions/47512043/how-etags-are-generated-and-configured>
-    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 cbdcbca..2648947 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<GetResult> {
         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(),
         })

Reply via email to