1use object_store::path::Path;
5
6use crate::{Error, Result};
7
8pub fn validate_managed_reference(uri: &str, position: u64, size: u64) -> Result<Path> {
10 if uri.is_empty()
11 || uri.starts_with('/')
12 || uri.ends_with('/')
13 || uri.contains("://")
14 || uri.contains('\\')
15 || uri
16 .split('/')
17 .any(|part| part.is_empty() || part == "." || part == "..")
18 {
19 return Err(Error::invalid_input(format!(
20 "Managed blob_uri must be a non-empty canonical relative object path, got {uri:?}"
21 )));
22 }
23 position.checked_add(size).ok_or_else(|| {
24 Error::invalid_input(format!(
25 "Managed blob range overflows u64: position={position}, size={size}"
26 ))
27 })?;
28 Path::parse(uri)
29 .map_err(|error| Error::invalid_input(format!("Invalid Managed blob_uri {uri:?}: {error}")))
30}
31
32pub fn blob_path(base: &Path, data_file_key: &str, blob_id: u32) -> Path {
39 let file_name = format!("{:032b}.blob", blob_id.reverse_bits());
40 base.clone().join(data_file_key).join(file_name.as_str())
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use rstest::rstest;
47
48 #[rstest]
49 #[case("")]
50 #[case("/data/a.blob")]
51 #[case("data/../a.blob")]
52 #[case("data/./a.blob")]
53 #[case("data//a.blob")]
54 #[case("data/a.blob/")]
55 #[case("s3://bucket/a.blob")]
56 #[case("data\\a.blob")]
57 fn managed_paths_reject_noncanonical_references(#[case] uri: &str) {
58 let error = validate_managed_reference(uri, 0, 1).unwrap_err();
59 assert!(matches!(error, Error::InvalidInput { .. }));
60 assert!(error.to_string().contains("Managed blob_uri"));
61 }
62
63 #[test]
64 fn managed_ranges_preserve_empty_values_and_reject_overflow() {
65 assert!(validate_managed_reference("data/a.blob", u64::MAX, 0).is_ok());
66 let error = validate_managed_reference("data/a.blob", u64::MAX, 1).unwrap_err();
67 assert!(matches!(error, Error::InvalidInput { .. }));
68 assert!(
69 error
70 .to_string()
71 .contains("position=18446744073709551615, size=1")
72 );
73 }
74
75 #[test]
76 fn test_blob_path_formatting() {
77 let base = Path::from("base");
78 let path = blob_path(&base, "deadbeef", 2);
79 assert_eq!(
80 path.to_string(),
81 "base/deadbeef/01000000000000000000000000000000.blob"
82 );
83 }
84
85 #[test]
86 fn test_blob_path_scattered_prefixes_for_sequential_ids() {
87 let base = Path::from("base");
88 let p1 = blob_path(&base, "deadbeef", 1);
89 let p2 = blob_path(&base, "deadbeef", 2);
90 assert_ne!(p1.to_string(), p2.to_string());
91 assert_eq!(
92 p1.to_string(),
93 "base/deadbeef/10000000000000000000000000000000.blob"
94 );
95 assert_eq!(
96 p2.to_string(),
97 "base/deadbeef/01000000000000000000000000000000.blob"
98 );
99 }
100}