use std::sync::{Arc, RwLock};
use paimon::io::FileIO;
#[derive(Clone, Debug)]
struct BlobFileIO {
prefix: String,
file_io: FileIO,
}
#[derive(Clone, Debug, Default)]
pub struct BlobReaderRegistry {
readers: Arc<RwLock<Vec<BlobFileIO>>>,
}
fn allows_backslash_boundary(location: &str) -> bool {
let bytes = location.as_bytes();
location.starts_with("file:/")
|| (bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'\\' | b'/'))
}
fn matches_location(uri: &str, location: &str) -> bool {
uri == location
|| uri.strip_prefix(location).is_some_and(|suffix| {
location.ends_with('/')
|| suffix.starts_with('/')
|| (allows_backslash_boundary(location)
&& (location.ends_with('\\') || suffix.starts_with('\\')))
})
}
impl BlobReaderRegistry {
pub fn register(&self, prefix: impl Into<String>, file_io: FileIO) {
let prefix = prefix.into();
let mut readers = self.readers.write().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = readers.iter_mut().find(|reader| reader.prefix == prefix) {
existing.file_io = file_io;
return;
}
readers.push(BlobFileIO { prefix, file_io });
}
pub fn register_if_absent(&self, prefix: impl Into<String>, file_io: FileIO) {
let prefix = prefix.into();
let mut readers = self.readers.write().unwrap_or_else(|e| e.into_inner());
if readers.iter().any(|reader| reader.prefix == prefix) {
return;
}
readers.push(BlobFileIO { prefix, file_io });
}
pub fn resolve(&self, uri: &str) -> Option<FileIO> {
let readers = self.readers.read().unwrap_or_else(|e| e.into_inner());
readers
.iter()
.filter(|reader| matches_location(uri, &reader.prefix))
.max_by_key(|reader| reader.prefix.len())
.map(|reader| reader.file_io.clone())
}
}
#[cfg(test)]
mod tests {
use std::fs;
use paimon::io::{FileIOBuilder, FileRead};
use paimon::spec::BlobDescriptor;
use super::*;
fn memory_file_io() -> FileIO {
FileIOBuilder::new("memory").build().unwrap()
}
async fn write_file(file_io: &FileIO, uri: &str, contents: &[u8]) {
file_io
.new_output(uri)
.unwrap()
.write(contents.to_vec().into())
.await
.unwrap();
}
async fn assert_resolves_to(registry: &BlobReaderRegistry, uri: &str, expected: &[u8]) {
let file_io = registry
.resolve(uri)
.unwrap_or_else(|| panic!("expected registered FileIO for {uri}"));
let bytes = file_io.new_input(uri).unwrap().read().await.unwrap();
assert_eq!(&bytes[..], expected);
}
#[test]
fn resolves_only_same_location_or_descendants() {
let cases = [
("memory:/foo", "memory:/foo", true),
("memory:/foo", "memory:/foo/blob.bin", true),
("memory:/foo", "memory:/foo/nested/blob.bin", true),
("memory:/foo", "memory:/foobar/private.bin", false),
("s3://bucket", "s3://bucket-other/blob.bin", false),
("s3://bucket/table", "gs://bucket/table/blob.bin", false),
("memory:/foo/", "memory:/foo/", true),
("memory:/foo/", "memory:/foo/blob.bin", true),
("memory:/foo/", "memory:/foo", false),
(r"C:\warehouse\table", r"C:\warehouse\table\blob.bin", true),
(
r"C:\warehouse\table",
r"C:\warehouse\table-other\blob.bin",
false,
),
(
"C:\\warehouse\\table\\",
r"C:\warehouse\table\blob.bin",
true,
),
("C:\\warehouse\\table\\", r"C:\warehouse\table", false),
(
r"file:/C:\warehouse\table",
r"file:/C:\warehouse\table\blob.bin",
true,
),
(
r"file:/C:\warehouse\table",
r"file:/C:\warehouse\table-other\blob.bin",
false,
),
("s3://bucket/table", r"s3://bucket/table\blob.bin", false),
];
for (location, uri, expected) in cases {
let registry = BlobReaderRegistry::default();
registry.register(location, memory_file_io());
assert_eq!(
registry.resolve(uri).is_some(),
expected,
"location={location}, uri={uri}"
);
}
}
#[tokio::test]
async fn resolves_with_longest_valid_location() {
let parent_file_io = memory_file_io();
let child_file_io = memory_file_io();
let registry = BlobReaderRegistry::default();
registry.register("memory:/warehouse/db", parent_file_io.clone());
registry.register("memory:/warehouse/db/foo", child_file_io.clone());
let child_uri = "memory:/warehouse/db/foo/blob.bin";
write_file(&parent_file_io, child_uri, b"parent").await;
write_file(&child_file_io, child_uri, b"child").await;
assert_resolves_to(®istry, child_uri, b"child").await;
let sibling_uri = "memory:/warehouse/db/foobar/private.bin";
write_file(&parent_file_io, sibling_uri, b"parent").await;
write_file(&child_file_io, sibling_uri, b"child").await;
assert_resolves_to(®istry, sibling_uri, b"parent").await;
}
#[tokio::test]
async fn preserves_registration_semantics() {
let uri = "memory:/warehouse/db/foo";
let initial_file_io = memory_file_io();
let replacement_file_io = memory_file_io();
write_file(&initial_file_io, uri, b"initial").await;
write_file(&replacement_file_io, uri, b"replacement").await;
let registry = BlobReaderRegistry::default();
registry.register(uri, initial_file_io.clone());
registry.register(uri, replacement_file_io.clone());
assert_resolves_to(®istry, uri, b"replacement").await;
let registry = BlobReaderRegistry::default();
registry.register(uri, initial_file_io);
registry.register_if_absent(uri, replacement_file_io);
assert_resolves_to(®istry, uri, b"initial").await;
}
#[tokio::test]
async fn resolves_file_blob_descriptor_with_file_io() {
let directory = tempfile::tempdir().unwrap();
let blob_path = directory.path().join("blob.bin");
fs::write(&blob_path, b"prefixpayloadsuffix").unwrap();
let descriptor = BlobDescriptor::new(
blob_path.to_string_lossy().to_string(),
6,
"payload".len() as i64,
);
let descriptor = BlobDescriptor::deserialize(&descriptor.serialize()).unwrap();
let registry = BlobReaderRegistry::default();
let file_io = FileIOBuilder::new("file").build().unwrap();
registry.register(directory.path().to_string_lossy().to_string(), file_io);
let resolved_file_io = registry
.resolve(descriptor.uri())
.expect("file blob descriptor should resolve to registered FileIO");
let input = resolved_file_io.new_input(descriptor.uri()).unwrap();
let reader = input.reader().await.unwrap();
let start = descriptor.offset() as u64;
let end = start + descriptor.length() as u64;
let bytes = reader.read(start..end).await.unwrap();
assert_eq!(&bytes[..], b"payload");
}
}