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(),
})