This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 295f545f fix(deletion_vector): read Java bitmap64 deletion vectors
(#785)
295f545f is described below
commit 295f545f3106b19fb6570db97303ab300d4f27c1
Author: jackylee <[email protected]>
AuthorDate: Fri Sep 18 14:46:34 2026 +0800
fix(deletion_vector): read Java bitmap64 deletion vectors (#785)
---
crates/paimon/src/deletion_vector/core.rs | 393 ++++++++++++++++++++++++---
crates/paimon/src/deletion_vector/factory.rs | 91 ++++++-
2 files changed, 443 insertions(+), 41 deletions(-)
diff --git a/crates/paimon/src/deletion_vector/core.rs
b/crates/paimon/src/deletion_vector/core.rs
index 4164da64..99f700f2 100644
--- a/crates/paimon/src/deletion_vector/core.rs
+++ b/crates/paimon/src/deletion_vector/core.rs
@@ -32,7 +32,15 @@ pub struct DeletionVector {
/// Magic number for BitmapDeletionVector serialization format
/// Same as Java: 1581511376
const MAGIC_NUMBER: u32 = 1581511376;
-const MAGIC_NUMBER_SIZE_BYTES: usize = 4;
+/// Magic number for Java `Bitmap64DeletionVector` (v2). Written little-endian,
+/// unlike the v1 magic, so the two are told apart by byte order as well as
value.
+const MAGIC_NUMBER_64: u32 = 1681511377;
+const MAGIC_NUMBER_SIZE_BYTES: u64 = 4;
+/// Size of the `bitmapLength` prefix and of the trailing CRC. Mirrors Java
+/// `Bitmap64DeletionVector.LENGTH_SIZE_BYTES` / `CRC_SIZE_BYTES`; a v2 entry
+/// counts both in `DeletionFile.length()` while a v1 entry counts neither.
+const LENGTH_SIZE_BYTES: u64 = 4;
+const CRC_SIZE_BYTES: u64 = 4;
impl DeletionVector {
/// Create a new empty DeletionVector
@@ -96,12 +104,10 @@ impl DeletionVector {
source: Some(Box::new(e)),
})?;
- let bitmap_length =
- i32::try_from(MAGIC_NUMBER_SIZE_BYTES +
bitmap_bytes.len()).map_err(|_| {
- crate::Error::DataInvalid {
- message: "Deletion vector bitmap is too large to
serialize".to_string(),
- source: None,
- }
+ let bitmap_length = i32::try_from(MAGIC_NUMBER_SIZE_BYTES as usize +
bitmap_bytes.len())
+ .map_err(|_| crate::Error::DataInvalid {
+ message: "Deletion vector bitmap is too large to
serialize".to_string(),
+ source: None,
})?;
let mut payload = Vec::with_capacity(8 + bitmap_bytes.len() + 4);
@@ -121,13 +127,24 @@ impl DeletionVector {
&self.bitmap
}
- /// Read a DeletionVector from bytes, similar to Java
DeletionVector.read(DataInputStream, length)
+ /// Read a DeletionVector from bytes, mirroring Java
`DeletionVector.read(DataInputStream, length)`,
+ /// which accepts two on-disk formats and dispatches on the magic number.
+ ///
+ /// `BitmapDeletionVector` (v1):
+ /// - `bitmapLength` (i32 big-endian): magic + bitmap data
+ /// - magic [`MAGIC_NUMBER`] (i32 big-endian)
+ /// - bitmap data (`bitmapLength - 4` bytes): serialized roaring32 bitmap
+ /// - CRC (i32): not verified here, matching Java's `dis.skipBytes(4)`
///
- /// Format (as read by DeletionVector.read):
- /// - bitmapLength (4 bytes int): total size including magic
- /// - magicNumber (4 bytes int): BitmapDeletionVector.MAGIC_NUMBER
- /// - bitmap data (bitmapLength - 4 bytes): serialized RoaringBitmap
- /// - CRC (4 bytes): checksum (skipped during read)
+ /// `Bitmap64DeletionVector` (v2), whose payload is little-endian:
+ /// - `bitmapLength` (i32 **big**-endian): magic + bitmap data
+ /// - magic [`MAGIC_NUMBER_64`] (i32 **little**-endian)
+ /// - bitmap data: portable 64-bit roaring (`u64` bucket count, then `u32`
key
+ /// plus a roaring32 bitmap per bucket), possibly run-length encoded
+ /// - CRC (i32): not verified here either
+ ///
+ /// `expected_length` is `DeletionFile.length()`. The two formats count it
+ /// differently: v1 excludes the length prefix and the CRC, v2 includes
both.
pub fn read_from_bytes(bytes: &[u8], expected_length: Option<u64>) ->
crate::Result<Self> {
use bytes::Buf;
if bytes.len() < 8 {
@@ -139,63 +156,187 @@ impl DeletionVector {
let mut buf = bytes;
- // Read bitmapLength (total size including magic)
- let bitmap_length = buf.get_i32() as usize;
+ // Read bitmapLength (magic + bitmap data). Both formats store it
+ // big-endian. Reject a negative value here so the size arithmetic
below
+ // cannot wrap: `as usize` would turn -1 into u64::MAX and make the
+ // "data incomplete" guard compute a tiny requirement and pass.
+ let bitmap_length =
+ u64::try_from(buf.get_i32()).map_err(|_| crate::Error::DataInvalid
{
+ message: "Deletion vector bitmap length is
negative".to_string(),
+ source: None,
+ })?;
- // Read magic number
+ // Read magic number. v1 is big-endian, v2 little-endian.
let magic_number = buf.get_i32() as u32;
- if magic_number != MAGIC_NUMBER {
+ let is_bitmap64 = if magic_number == MAGIC_NUMBER {
+ false
+ } else if magic_number.swap_bytes() == MAGIC_NUMBER_64 {
+ true
+ } else {
return Err(crate::Error::DataInvalid {
message: format!(
- "Invalid magic number: expected {MAGIC_NUMBER}, got
{magic_number}"
+ "Invalid magic number: {magic_number}, \
+ v1 dv magic number: {MAGIC_NUMBER}, v2 magic number:
{MAGIC_NUMBER_64}"
),
source: None,
});
- }
+ };
- // Verify length if provided
+ // Verify length if provided, using each format's own convention.
if let Some(expected) = expected_length {
- if bitmap_length as u64 != expected {
+ let expected_bitmap_length = if is_bitmap64 {
+ expected
+ .checked_sub(LENGTH_SIZE_BYTES + CRC_SIZE_BYTES)
+ .ok_or_else(|| crate::Error::DataInvalid {
+ message: format!("Deletion vector length {expected} is
too small"),
+ source: None,
+ })?
+ } else {
+ expected
+ };
+ if bitmap_length != expected_bitmap_length {
return Err(crate::Error::DataInvalid {
message: format!(
- "Size not match, actual size: {bitmap_length},
expected size: {expected}"
+ "Size not match, actual size: {bitmap_length},
expected size: {expected_bitmap_length}"
),
source: None,
});
}
}
- // Read bitmap data (bitmapLength - 4 bytes, since magic is already
included in bitmapLength)
- let bitmap_data_size = bitmap_length - MAGIC_NUMBER_SIZE_BYTES;
- // 4(bitmap_length) + 4(magic_number) + bitmap_data_size + 4(crc)
- if bytes.len() < 8 + bitmap_data_size + 4 {
+ // Bitmap data follows the magic, which bitmapLength counts.
+ let bitmap_data_size = bitmap_length
+ .checked_sub(MAGIC_NUMBER_SIZE_BYTES)
+ .and_then(|size| usize::try_from(size).ok())
+ .ok_or_else(|| crate::Error::DataInvalid {
+ message: format!("Deletion vector bitmap length
{bitmap_length} is too small"),
+ source: None,
+ })?;
+ // The CRC is not verified, so it need not be present: a v2 entry that
+ // ends the index file cannot be over-read (see
`DeletionVectorFactory::read`).
+ let needed = 8 + bitmap_data_size;
+ if bytes.len() < needed {
return Err(crate::Error::DataInvalid {
message: format!(
- "Deletion vector data incomplete: need {} bytes, got {}",
- 8 + bitmap_data_size + 4,
+ "Deletion vector data incomplete: need {needed} bytes, got
{}",
bytes.len()
),
source: None,
});
}
- let bitmap_data = &bytes[8..8 + bitmap_data_size];
+ let bitmap_data = &bytes[8..needed];
+ if is_bitmap64 {
+ Self::from_bitmap64_bytes(bitmap_data)
+ } else {
+ let bitmap =
RoaringBitmap::deserialize_from(bitmap_data).map_err(|e| {
+ crate::Error::DataInvalid {
+ message: format!("Failed to deserialize RoaringBitmap:
{e}"),
+ source: Some(Box::new(e)),
+ }
+ })?;
+ Ok(Self::from_bitmap(bitmap))
+ }
+ }
- // Skip CRC (4 bytes) - Java code does: dis.skipBytes(4)
- // We don't need to verify it here as it's skipped
+ /// Decode a `Bitmap64DeletionVector` payload into the roaring32
representation
+ /// this type stores.
+ ///
+ /// Row positions are offsets inside a single data file, and every
existing API
+ /// here is already roaring32-bound -- [`Self::is_deleted`] documents that
+ /// positions above `u32::MAX` cannot be present, and `to_bitmap` hands a
+ /// `RoaringBitmap` to the writer. A position that does not fit is
therefore
+ /// rejected rather than truncated, so a 64-bit vector can never silently
lose
+ /// deletes; a data file with more than `u32::MAX` rows would be needed to
+ /// reach it.
+ fn from_bitmap64_bytes(bitmap_data: &[u8]) -> crate::Result<Self> {
+ // The outer layer is decoded here rather than through
+ // `RoaringTreemap::deserialize_from`, which validates each inner
roaring32
+ // bitmap but inserts the bucket keys straight into a `BTreeMap` -- a
+ // duplicate key silently replaces the earlier bucket and its deletes
are
+ // lost. Java's `OptimizedRoaringBitmap64.deserialize` rejects that,
so the
+ // count and every key are checked the same way here
+ // (`readBitmapCount` / `readKey`).
+ let mut cursor = bitmap_data;
+ let bucket_count = read_u64_le(&mut cursor)?;
+ if bucket_count > i32::MAX as u64 {
+ return Err(crate::Error::DataInvalid {
+ message: format!("Invalid deletion vector bitmap count:
{bucket_count}"),
+ source: None,
+ });
+ }
- // Deserialize RoaringBitmap
- let bitmap = RoaringBitmap::deserialize_from(bitmap_data).map_err(|e| {
- crate::Error::DataInvalid {
- message: format!("Failed to deserialize RoaringBitmap: {e}"),
- source: Some(Box::new(e)),
+ let mut bitmap = RoaringBitmap::new();
+ let mut last_key: Option<u32> = None;
+ for _ in 0..bucket_count {
+ let key = read_u32_le(&mut cursor)?;
+ if key > (i32::MAX as u32 - 1) {
+ return Err(crate::Error::DataInvalid {
+ message: format!("Deletion vector bitmap key is too large:
{key}"),
+ source: None,
+ });
}
- })?;
+ if last_key.is_some_and(|previous| key <= previous) {
+ return Err(crate::Error::DataInvalid {
+ message: "Deletion vector bitmap keys must be sorted in
ascending order"
+ .to_string(),
+ source: None,
+ });
+ }
+ last_key = Some(key);
+ let bucket = RoaringBitmap::deserialize_from(&mut
cursor).map_err(|e| {
+ crate::Error::DataInvalid {
+ message: format!("Failed to deserialize 64-bit
RoaringBitmap bucket: {e}"),
+ source: Some(Box::new(e)),
+ }
+ })?;
+ if key > 0 && !bucket.is_empty() {
+ // Positions in bucket `key` are `key << 32 | low`, so anything
+ // above bucket 0 exceeds `u32::MAX`. Rejected rather than
+ // truncated: every API here is roaring32-bound, and reaching
one
+ // needs a data file with more than `u32::MAX` rows.
+ let position = (u64::from(key) << 32) |
u64::from(bucket.min().unwrap_or(0));
+ return Err(crate::Error::DataInvalid {
+ message: format!(
+ "Deletion vector position {position} exceeds u32::MAX,
\
+ which this reader cannot represent"
+ ),
+ source: None,
+ });
+ }
+ bitmap |= bucket;
+ }
Ok(Self::from_bitmap(bitmap))
}
}
+/// Read a little-endian `u64`, advancing `cursor`.
+fn read_u64_le(cursor: &mut &[u8]) -> crate::Result<u64> {
+ let bytes: [u8; 8] = cursor
+ .get(..8)
+ .and_then(|slice| slice.try_into().ok())
+ .ok_or_else(|| crate::Error::DataInvalid {
+ message: "Deletion vector bitmap data truncated".to_string(),
+ source: None,
+ })?;
+ *cursor = &cursor[8..];
+ Ok(u64::from_le_bytes(bytes))
+}
+
+/// Read a little-endian `u32`, advancing `cursor`.
+fn read_u32_le(cursor: &mut &[u8]) -> crate::Result<u32> {
+ let bytes: [u8; 4] = cursor
+ .get(..4)
+ .and_then(|slice| slice.try_into().ok())
+ .ok_or_else(|| crate::Error::DataInvalid {
+ message: "Deletion vector bitmap data truncated".to_string(),
+ source: None,
+ })?;
+ *cursor = &cursor[4..];
+ Ok(u32::from_le_bytes(bytes))
+}
+
impl Default for DeletionVector {
fn default() -> Self {
Self::empty()
@@ -259,6 +400,184 @@ mod tests {
assert_eq!(dv.bitmap(), &expected_bitmap, "bitmap should be [1, 2]");
}
+ /// Build the on-disk bytes Java `Bitmap64DeletionVector.serializeTo`
writes:
+ /// `i32 BE bitmapLength | i32 LE magic | portable 64-bit roaring | i32 BE
crc`,
+ /// where the payload is `u64 LE bucket count` then `u32 LE key` plus a
+ /// roaring32 bitmap per bucket, densely for every key from 0 upwards.
+ fn java_bitmap64_bytes(buckets: &[&[u32]], run_length_encode: bool) ->
Vec<u8> {
+ let mut payload = Vec::new();
+ payload.extend_from_slice(&(buckets.len() as u64).to_le_bytes());
+ for (key, values) in buckets.iter().enumerate() {
+ payload.extend_from_slice(&(key as u32).to_le_bytes());
+ let mut bitmap = RoaringBitmap::new();
+ for value in *values {
+ bitmap.insert(*value);
+ }
+ if run_length_encode {
+ // Java calls `roaringBitmap.runLengthEncode()` before
serializing.
+ bitmap.optimize();
+ }
+ bitmap.serialize_into(&mut payload).unwrap();
+ }
+
+ let bitmap_length = 4 + payload.len();
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&(bitmap_length as i32).to_be_bytes());
+ bytes.extend_from_slice(&MAGIC_NUMBER_64.to_le_bytes());
+ bytes.extend_from_slice(&payload);
+ let mut crc = crc32fast::Hasher::new();
+ crc.update(&bytes[4..]);
+ bytes.extend_from_slice(&(crc.finalize() as i32).to_be_bytes());
+ bytes
+ }
+
+ /// `DeletionFile.length()` for a v2 entry is the whole on-disk run.
+ fn bitmap64_declared_length(bytes: &[u8]) -> u64 {
+ bytes.len() as u64
+ }
+
+ /// Wrap a bitmap64 payload in the on-disk envelope Java writes.
+ fn wrap_bitmap64_payload(payload: &[u8]) -> Vec<u8> {
+ let bitmap_length = 4 + payload.len();
+ let mut bytes = (bitmap_length as i32).to_be_bytes().to_vec();
+ bytes.extend_from_slice(&MAGIC_NUMBER_64.to_le_bytes());
+ bytes.extend_from_slice(payload);
+ let mut crc = crc32fast::Hasher::new();
+ crc.update(&bytes[4..]);
+ bytes.extend_from_slice(&(crc.finalize() as i32).to_be_bytes());
+ bytes
+ }
+
+ #[test]
+ fn test_read_bitmap64_deletion_vector() {
+ let bytes = java_bitmap64_bytes(&[&[1u32, 2u32, 9u32]], false);
+ let dv = DeletionVector::read_from_bytes(&bytes,
Some(bitmap64_declared_length(&bytes)))
+ .expect("v2 deletion vector must be readable");
+ assert_eq!(dv.bitmap(), &RoaringBitmap::from_iter([1u32, 2, 9]));
+ assert!(dv.is_deleted(2) && !dv.is_deleted(3));
+ assert_eq!(dv.cardinality(), 3);
+ }
+
+ #[test]
+ fn test_read_bitmap64_run_length_encoded() {
+ // Java run-length encodes before writing, which switches the container
+ // type and the serialization cookie.
+ let run: Vec<u32> = (100u32..400).collect();
+ let bytes = java_bitmap64_bytes(&[&run], true);
+ let dv = DeletionVector::read_from_bytes(&bytes,
Some(bitmap64_declared_length(&bytes)))
+ .expect("run-length encoded v2 deletion vector must be readable");
+ assert_eq!(dv.cardinality(), run.len() as u64);
+ assert!(dv.is_deleted(100) && dv.is_deleted(399) &&
!dv.is_deleted(400));
+ }
+
+ /// `RoaringTreemap::deserialize_from` would accept a duplicate bucket key
and
+ /// let the later bucket replace the earlier one, dropping its deletes;
Java
+ /// rejects it in `readKey`. Descending keys and a bogus count are the same
+ /// class of corruption.
+ #[test]
+ fn test_read_bitmap64_rejects_bad_bucket_keys() {
+ // Two buckets both keyed 0: {1} then {2}. Without the check this
decodes
+ // to {2} alone and position 1 reads as a live row.
+ let mut payload = 2u64.to_le_bytes().to_vec();
+ for values in [[1u32], [2u32]] {
+ payload.extend_from_slice(&0u32.to_le_bytes());
+ let mut bucket = RoaringBitmap::new();
+ bucket.insert(values[0]);
+ bucket.serialize_into(&mut payload).unwrap();
+ }
+ let bytes = wrap_bitmap64_payload(&payload);
+ let err = DeletionVector::read_from_bytes(&bytes, Some(bytes.len() as
u64))
+ .expect_err("a duplicate bucket key must be rejected");
+ assert!(err.to_string().contains("ascending order"), "got: {err}");
+
+ // Descending keys. Bucket 1 is left empty so the order check is what
+ // rejects this, not the `u32::MAX` guard.
+ let mut payload = 2u64.to_le_bytes().to_vec();
+ for (key, values) in [(1u32, &[][..]), (0u32, &[6u32][..])] {
+ payload.extend_from_slice(&key.to_le_bytes());
+ let mut bucket = RoaringBitmap::new();
+ for value in values {
+ bucket.insert(*value);
+ }
+ bucket.serialize_into(&mut payload).unwrap();
+ }
+ let bytes = wrap_bitmap64_payload(&payload);
+ let err = DeletionVector::read_from_bytes(&bytes, Some(bytes.len() as
u64))
+ .expect_err("descending bucket keys must be rejected");
+ assert!(err.to_string().contains("ascending order"), "got: {err}");
+
+ // A count larger than i32::MAX cannot be a real bucket count.
+ let payload = (i32::MAX as u64 + 1).to_le_bytes().to_vec();
+ let bytes = wrap_bitmap64_payload(&payload);
+ let err = DeletionVector::read_from_bytes(&bytes, Some(bytes.len() as
u64))
+ .expect_err("a bogus bucket count must be rejected");
+ assert!(
+ err.to_string()
+ .contains("Invalid deletion vector bitmap count"),
+ "got: {err}"
+ );
+
+ // A truncated payload must error, not panic.
+ let bytes = wrap_bitmap64_payload(&1u64.to_le_bytes()[..4]);
+ let err = DeletionVector::read_from_bytes(&bytes, Some(bytes.len() as
u64))
+ .expect_err("a truncated payload must be rejected");
+ assert!(err.to_string().contains("truncated"), "got: {err}");
+ }
+
+ #[test]
+ fn test_read_bitmap64_with_empty_leading_bucket() {
+ // Java writes every key from 0 densely, so bucket 0 is present and
empty
+ // when no position below 2^32 is deleted. Rust's own writer omits it.
+ let bytes = java_bitmap64_bytes(&[&[], &[7u32]], false);
+ let err = DeletionVector::read_from_bytes(&bytes,
Some(bitmap64_declared_length(&bytes)))
+ .expect_err("a position above u32::MAX must be rejected, not
truncated");
+ assert!(
+ err.to_string().contains("exceeds u32::MAX"),
+ "unexpected error: {err}"
+ );
+ }
+
+ #[test]
+ fn test_read_bitmap64_uses_its_own_length_convention() {
+ let bytes = java_bitmap64_bytes(&[&[5u32]], false);
+ let declared = bitmap64_declared_length(&bytes);
+ // v1's convention (length == bitmapLength) must not be applied to v2.
+ let err = DeletionVector::read_from_bytes(&bytes, Some(declared - 8))
+ .expect_err("v2 length is the whole on-disk run, prefix and CRC
included");
+ assert!(err.to_string().contains("Size not match"), "got: {err}");
+ assert!(DeletionVector::read_from_bytes(&bytes,
Some(declared)).is_ok());
+ }
+
+ #[test]
+ fn test_read_rejects_unknown_magic_naming_both_formats() {
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&8i32.to_be_bytes());
+ bytes.extend_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
+ bytes.extend_from_slice(&[0u8; 8]);
+ let err = DeletionVector::read_from_bytes(&bytes,
None).expect_err("magic must be checked");
+ let message = err.to_string();
+ assert!(
+ message.contains(&MAGIC_NUMBER.to_string()),
+ "got: {message}"
+ );
+ assert!(
+ message.contains(&MAGIC_NUMBER_64.to_string()),
+ "got: {message}"
+ );
+ }
+
+ #[test]
+ fn test_read_rejects_negative_bitmap_length() {
+ // Taken straight from the file, a negative length used to become a
huge
+ // usize and wrap the "data incomplete" guard into passing.
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&(-1i32).to_be_bytes());
+ bytes.extend_from_slice(&MAGIC_NUMBER.to_be_bytes());
+ bytes.extend_from_slice(&[0u8; 8]);
+ let err = DeletionVector::read_from_bytes(&bytes,
None).expect_err("must not panic");
+ assert!(err.to_string().contains("negative"), "got: {err}");
+ }
+
#[test]
fn test_is_deleted_reports_membership_and_guards_u32_overflow() {
let mut bitmap = RoaringBitmap::new();
diff --git a/crates/paimon/src/deletion_vector/factory.rs
b/crates/paimon/src/deletion_vector/factory.rs
index cb5a47eb..6c640cc4 100644
--- a/crates/paimon/src/deletion_vector/factory.rs
+++ b/crates/paimon/src/deletion_vector/factory.rs
@@ -68,10 +68,93 @@ impl DeletionVectorFactory {
let reader = input.reader().await?;
let offset = df.offset() as u64;
let len = df.length() as u64;
- let bytes = reader
- // 4 bytes for bitmap length, 4 bytes for magic number
- .read(offset..offset.saturating_add(len).saturating_add(8))
- .await?;
+ // A v1 entry occupies `len + 8` bytes on disk while a v2 entry
occupies
+ // exactly `len` -- v2 counts the length prefix and the CRC in
+ // `DeletionFile.length()`, v1 counts neither (Java
`DeletionVector.read`).
+ // Neither format needs its CRC, so `len + 4` is the most either can
+ // require. Reading that much overruns a v2 entry that ends the index
+ // file, and a range past EOF is rejected outright rather than
truncated,
+ // so retry with the v2 size. v1 always satisfies the first range, so
it
+ // still costs a single read.
+ let wanted = offset..offset.saturating_add(len).saturating_add(4);
+ let bytes = match reader.read(wanted).await {
+ Ok(bytes) => bytes,
+ // The two ranges differ by 4 bytes, so an unrelated I/O failure
+ // fails this one too and its error is what surfaces.
+ Err(_) => reader.read(offset..offset.saturating_add(len)).await?,
+ };
DeletionVector::read_from_bytes(&bytes, Some(len))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::io::FileIO;
+ use bytes::Bytes;
+ use roaring::RoaringBitmap;
+
+ fn local_file_path(path: &std::path::Path) -> String {
+ let normalized = path.to_string_lossy().replace('\\', "/");
+ if normalized.starts_with('/') {
+ format!("file:{normalized}")
+ } else {
+ format!("file:/{normalized}")
+ }
+ }
+
+ /// One `Bitmap64DeletionVector` run as Java writes it:
+ /// `i32 BE bitmapLength | i32 LE magic | portable 64-bit roaring | i32 BE
crc`.
+ fn java_bitmap64_entry(values: &[u32]) -> Vec<u8> {
+ let mut bitmap = RoaringBitmap::new();
+ for value in values {
+ bitmap.insert(*value);
+ }
+ let mut payload = 1u64.to_le_bytes().to_vec();
+ payload.extend_from_slice(&0u32.to_le_bytes());
+ bitmap.serialize_into(&mut payload).unwrap();
+
+ let mut bytes = ((4 + payload.len()) as i32).to_be_bytes().to_vec();
+ bytes.extend_from_slice(&1681511377u32.to_le_bytes());
+ bytes.extend_from_slice(&payload);
+ let mut crc = crc32fast::Hasher::new();
+ crc.update(&bytes[4..]);
+ bytes.extend_from_slice(&(crc.finalize() as i32).to_be_bytes());
+ bytes
+ }
+
+ /// A v2 vector that ends the index file must still be readable. Its
+ /// `DeletionFile.length()` covers the whole run, so reading `length + 8`
+ /// runs past EOF and the storage layer rejects the range outright.
+ #[test]
+ fn test_read_bitmap64_at_end_of_index_file() {
+ let entry = java_bitmap64_entry(&[3u32, 11u32]);
+ let dir = tempfile::tempdir().unwrap();
+ let file_io = FileIO::from_path(dir.path().to_string_lossy())
+ .unwrap()
+ .build()
+ .unwrap();
+ let path = local_file_path(&dir.path().join("index-bitmap64-last"));
+
+ // Index files start with a one-byte version, then the vectors back to
back.
+ let mut file = vec![1u8];
+ file.extend_from_slice(&entry);
+
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ rt.block_on(async {
+ file_io
+ .new_output(&path)
+ .unwrap()
+ .write(Bytes::from(file))
+ .await
+ .unwrap();
+ let df = crate::DeletionFile::new(path.clone(), 1, entry.len() as
i64, Some(2));
+ let dv = DeletionVectorFactory::read(&file_io, &df)
+ .await
+ .expect("v2 vector at end of file must be readable");
+ assert!(dv.is_deleted(3) && dv.is_deleted(11));
+ assert!(!dv.is_deleted(4));
+ assert_eq!(dv.cardinality(), 2);
+ });
+ }
+}