Skip to content

Commit d9eaded

Browse files
authored
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.
1 parent de0029a commit d9eaded

3 files changed

Lines changed: 16 additions & 16 deletions

File tree

src/lib.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,7 +2492,7 @@ mod tests {
24922492
location: Path::from("test"),
24932493
last_modified: Utc.timestamp_nanos(100),
24942494
size: 100,
2495-
e_tag: Some("123".to_string()),
2495+
e_tag: Some("\"123\"".to_string()),
24962496
version: None,
24972497
};
24982498

@@ -2521,16 +2521,16 @@ mod tests {
25212521

25222522
options = GetOptions::default();
25232523

2524-
options.if_match = Some("123".to_string());
2524+
options.if_match = Some("\"123\"".to_string());
25252525
options.check_preconditions(&meta).unwrap();
25262526

2527-
options.if_match = Some("123,354".to_string());
2527+
options.if_match = Some("\"123\",\"354\"".to_string());
25282528
options.check_preconditions(&meta).unwrap();
25292529

2530-
options.if_match = Some("354, 123,".to_string());
2530+
options.if_match = Some("\"354\", \"123\"".to_string());
25312531
options.check_preconditions(&meta).unwrap();
25322532

2533-
options.if_match = Some("354".to_string());
2533+
options.if_match = Some("\"354\"".to_string());
25342534
options.check_preconditions(&meta).unwrap_err();
25352535

25362536
options.if_match = Some("*".to_string());
@@ -2542,16 +2542,16 @@ mod tests {
25422542

25432543
options = GetOptions::default();
25442544

2545-
options.if_none_match = Some("123".to_string());
2545+
options.if_none_match = Some("\"123\"".to_string());
25462546
options.check_preconditions(&meta).unwrap_err();
25472547

25482548
options.if_none_match = Some("*".to_string());
25492549
options.check_preconditions(&meta).unwrap_err();
25502550

2551-
options.if_none_match = Some("1232".to_string());
2551+
options.if_none_match = Some("\"1232\"".to_string());
25522552
options.check_preconditions(&meta).unwrap();
25532553

2554-
options.if_none_match = Some("23, 123".to_string());
2554+
options.if_none_match = Some("\"23\", \"123\"".to_string());
25552555
options.check_preconditions(&meta).unwrap_err();
25562556

25572557
// If-None-Match takes precedence

src/local.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,7 @@ fn get_etag(metadata: &Metadata) -> String {
14181418
// Use an ETag scheme based on that used by many popular HTTP servers
14191419
// <https://httpd.apache.org/docs/2.2/mod/core.html#fileetag>
14201420
// <https://stackoverflow.com/questions/47512043/how-etags-are-generated-and-configured>
1421-
format!("{inode:x}-{mtime:x}-{size:x}")
1421+
format!("\"{inode:x}-{mtime:x}-{size:x}\"")
14221422
}
14231423

14241424
fn convert_metadata(metadata: Metadata, location: Path) -> ObjectMeta {

src/memory.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ impl Storage {
157157
source: format!("Object at location {location} not found").into(),
158158
}),
159159
Some(e) => {
160-
let existing = e.e_tag.to_string();
160+
let existing = format!("\"{}\"", e.e_tag);
161161
let expected = v.e_tag.ok_or(Error::MissingETag)?;
162162
if existing == expected {
163163
*e = entry;
@@ -217,7 +217,7 @@ impl ObjectStore for InMemory {
217217
storage.next_etag += 1;
218218

219219
Ok(PutResult {
220-
e_tag: Some(etag.to_string()),
220+
e_tag: Some(format!("\"{}\"", etag)),
221221
version: None,
222222
extensions: Default::default(),
223223
})
@@ -238,7 +238,7 @@ impl ObjectStore for InMemory {
238238

239239
async fn get_opts(&self, location: &Path, options: GetOptions) -> Result<GetResult> {
240240
let entry = self.entry(location)?;
241-
let e_tag = entry.e_tag.to_string();
241+
let e_tag = format!("\"{}\"", entry.e_tag);
242242

243243
let meta = ObjectMeta {
244244
location: location.clone(),
@@ -331,7 +331,7 @@ impl ObjectStore for InMemory {
331331
location: key.clone(),
332332
last_modified: value.last_modified,
333333
size: value.data.len() as u64,
334-
e_tag: Some(value.e_tag.to_string()),
334+
e_tag: Some(format!("\"{}\"", value.e_tag)),
335335
version: None,
336336
})
337337
})
@@ -376,7 +376,7 @@ impl ObjectStore for InMemory {
376376
location: k.clone(),
377377
last_modified: v.last_modified,
378378
size: v.data.len() as u64,
379-
e_tag: Some(v.e_tag.to_string()),
379+
e_tag: Some(format!("\"{}\"", v.e_tag)),
380380
version: None,
381381
};
382382
objects.push(object);
@@ -480,7 +480,7 @@ impl MultipartStore for InMemory {
480480
}
481481
let etag = storage.insert(path, buf.into(), upload.attributes);
482482
Ok(PutResult {
483-
e_tag: Some(etag.to_string()),
483+
e_tag: Some(format!("\"{}\"", etag)),
484484
version: None,
485485
extensions: Default::default(),
486486
})
@@ -547,7 +547,7 @@ impl MultipartUpload for InMemoryUpload {
547547
);
548548

549549
Ok(PutResult {
550-
e_tag: Some(etag.to_string()),
550+
e_tag: Some(format!("\"{}\"", etag)),
551551
version: None,
552552
extensions: Default::default(),
553553
})

0 commit comments

Comments
 (0)