diff --git a/Cargo.lock b/Cargo.lock index 9fe6c1f8..9b4f9297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2608,6 +2608,7 @@ dependencies = [ "humantime-serde", "objectstore-metrics", "objectstore-types", + "regex", "reqwest 0.12.28", "sentry", "serde", diff --git a/Cargo.toml b/Cargo.toml index b498f8f8..e40fc863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ humantime-serde = "1.1.1" jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } mimalloc = { version = "0.1.48", features = ["v3", "override"] } rand = "0.9.1" +regex = "1.12.3" reqwest = { version = "0.12.22" } sentry = { version = "0.45.0" } serde = { version = "1.0.219", features = ["derive"] } diff --git a/objectstore-service/Cargo.toml b/objectstore-service/Cargo.toml index a2365067..9cd3b1c6 100644 --- a/objectstore-service/Cargo.toml +++ b/objectstore-service/Cargo.toml @@ -21,6 +21,7 @@ humantime = { workspace = true } humantime-serde = { workspace = true } objectstore-metrics = { workspace = true } objectstore-types = { workspace = true } +regex = { workspace = true } reqwest = { workspace = true, features = [ "hickory-dns", "stream", diff --git a/objectstore-service/src/backend/bigtable.rs b/objectstore-service/src/backend/bigtable.rs index eba9701f..ed20935f 100644 --- a/objectstore-service/src/backend/bigtable.rs +++ b/objectstore-service/src/backend/bigtable.rs @@ -233,6 +233,14 @@ fn tombstone_predicate() -> v2::RowFilter { } } +/// Builds an anchored regex pattern (`^…$`) that matches `value` literally. +/// +/// Uses [`regex::escape`] so that metacharacters in storage paths (`.`, `/`, etc.) +/// are treated as literal bytes. +fn exact_value_regex(value: &str) -> Vec { + format!("^{}$", regex::escape(value)).into_bytes() +} + /// Creates a row filter that matches tombstones whose redirect resolves to `target`. /// /// Always includes an exact match on the `r` (redirect) column: @@ -245,19 +253,14 @@ fn tombstone_predicate() -> v2::RowFilter { /// - Chain: `m` column present AND value matches `{"is_redirect_tombstone":true...}` regex /// (legacy metadata format predating the dedicated `r` column) fn redirect_target_filter(target: &ObjectId, own_id: &ObjectId) -> v2::RowFilter { - let target_path = target.as_storage_path().to_string().into_bytes(); + let target_path = exact_value_regex(&target.as_storage_path().to_string()); let exact_match = v2::RowFilter { filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain { filters: vec![ column_filter(COLUMN_REDIRECT), v2::RowFilter { - filter: Some(v2::row_filter::Filter::ValueRangeFilter(v2::ValueRange { - start_value: Some(v2::value_range::StartValue::StartValueClosed( - target_path.clone(), - )), - end_value: Some(v2::value_range::EndValue::EndValueClosed(target_path)), - })), + filter: Some(v2::row_filter::Filter::ValueRegexFilter(target_path)), }, ], })), @@ -272,10 +275,7 @@ fn redirect_target_filter(target: &ObjectId, own_id: &ObjectId) -> v2::RowFilter filters: vec![ column_filter(COLUMN_REDIRECT), v2::RowFilter { - filter: Some(v2::row_filter::Filter::ValueRangeFilter(v2::ValueRange { - start_value: Some(v2::value_range::StartValue::StartValueClosed(vec![])), - end_value: Some(v2::value_range::EndValue::EndValueClosed(vec![])), - })), + filter: Some(v2::row_filter::Filter::ValueRegexFilter(b"^$".to_vec())), }, ], })),