From d5802c766a6cc3bce6ea20c2f9deefb6c009f5f6 Mon Sep 17 00:00:00 2001 From: pr Date: Thu, 6 Aug 2026 15:56:22 +0200 Subject: [PATCH 1/5] fix(cbq): allow reading index of zero-record CBQ files --- src/cbq/core/index.rs | 7 +++++++ src/cbq/read.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/cbq/core/index.rs b/src/cbq/core/index.rs index b70dad7..e61657f 100644 --- a/src/cbq/core/index.rs +++ b/src/cbq/core/index.rs @@ -109,6 +109,13 @@ impl Index { /// Builds the index from a byte slice pub fn from_bytes(bytes: &[u8]) -> Result { + // A zero-record CBQ has an empty index whose decompressed buffer has + // a dangling pointer, which fails `try_cast_slice`'s alignment check. + if bytes.is_empty() { + return Ok(Self { + ranges: Vec::default(), + }); + } let ranges = match bytemuck::try_cast_slice(bytes) { Ok(ranges) => ranges.to_vec(), Err(_) => return Err(CbqError::IndexCastingError.into()), diff --git a/src/cbq/read.rs b/src/cbq/read.rs index 2058bf6..5228d52 100644 --- a/src/cbq/read.rs +++ b/src/cbq/read.rs @@ -380,6 +380,36 @@ mod tests { assert!(num_blocks > 0, "Should have at least one block"); } + // ==================== Empty File Tests ==================== + + /// A CBQ file containing zero records (e.g. produced when an upstream + /// filter discards every read) has an empty index. Reading the index + /// back must not fail: `bytemuck::try_cast_slice` rejects the empty + /// decompressed buffer because its dangling pointer is not aligned for + /// `BlockRange`, so `Index::from_bytes` needs an explicit empty guard. + #[test] + fn test_read_index_empty_file() { + use std::io::Cursor; + + use crate::cbq::{ColumnarBlockWriter, core::FileHeaderBuilder}; + + let header = FileHeaderBuilder::default() + .is_paired(false) + .with_headers(false) + .with_qualities(false) + .with_flags(false) + .with_block_size(64) + .build(); + let mut writer = ColumnarBlockWriter::new(Vec::new(), header).unwrap(); + writer.finish().unwrap(); + + let mut reader = Reader::new(Cursor::new(writer.inner_data().to_vec())).unwrap(); + while reader.read_block().unwrap().is_some() {} + let index = reader.read_index().unwrap().expect("index should exist"); + assert_eq!(index.num_records(), 0); + assert_eq!(index.num_blocks(), 0); + } + // ==================== Default Quality Score Tests ==================== #[test] From f1b5f7406029299c259be61442b3887b5b756fe6 Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:26:04 -0700 Subject: [PATCH 2/5] dep: added tempfile to dev dependencies --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 01b01e1..fcbb2f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ anyhow = "1.0.103" parking_lot = "0.12.5" clap = { version = "4.6.2", features = ["derive"] } paraseq = "0.4.14" +tempfile = "3.27.0" [features] default = ["paraseq", "anyhow"] From 841a84b8f85ff49b5424bc3c71629bf91df08ff5 Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:26:27 -0700 Subject: [PATCH 3/5] tests: extend tests for empty file with mmap and stream --- src/cbq/read.rs | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/cbq/read.rs b/src/cbq/read.rs index 5228d52..9d16cf2 100644 --- a/src/cbq/read.rs +++ b/src/cbq/read.rs @@ -332,6 +332,10 @@ impl ParallelReader for MmapReader { } #[cfg(test)] mod tests { + use std::io::Write; + + use tempfile::NamedTempFile; + use super::*; use crate::BinseqRecord; @@ -382,17 +386,8 @@ mod tests { // ==================== Empty File Tests ==================== - /// A CBQ file containing zero records (e.g. produced when an upstream - /// filter discards every read) has an empty index. Reading the index - /// back must not fail: `bytemuck::try_cast_slice` rejects the empty - /// decompressed buffer because its dangling pointer is not aligned for - /// `BlockRange`, so `Index::from_bytes` needs an explicit empty guard. - #[test] - fn test_read_index_empty_file() { - use std::io::Cursor; - + fn make_empty_cbq() -> Vec { use crate::cbq::{ColumnarBlockWriter, core::FileHeaderBuilder}; - let header = FileHeaderBuilder::default() .is_paired(false) .with_headers(false) @@ -402,14 +397,40 @@ mod tests { .build(); let mut writer = ColumnarBlockWriter::new(Vec::new(), header).unwrap(); writer.finish().unwrap(); + writer.inner_data().to_vec() + } + + /// A CBQ file containing zero records (e.g. produced when an upstream + /// filter discards every read) has an empty index. Reading the index + /// back must not fail: `bytemuck::try_cast_slice` rejects the empty + /// decompressed buffer because its dangling pointer is not aligned for + /// `BlockRange`, so `Index::from_bytes` needs an explicit empty guard. + #[test] + fn test_read_index_empty_file() { + use std::io::Cursor; - let mut reader = Reader::new(Cursor::new(writer.inner_data().to_vec())).unwrap(); + let empty_cbq = make_empty_cbq(); + let mut reader = Reader::new(Cursor::new(empty_cbq)).unwrap(); while reader.read_block().unwrap().is_some() {} let index = reader.read_index().unwrap().expect("index should exist"); assert_eq!(index.num_records(), 0); assert_eq!(index.num_blocks(), 0); } + #[test] + fn test_read_index_empty_file_mmap() { + let empty_cbq = make_empty_cbq(); + + let ntf = NamedTempFile::new().unwrap(); + let (mut tmpfile, tmppath) = ntf.into_parts(); + tmpfile.write_all(&empty_cbq).unwrap(); + + let reader = MmapReader::new(tmppath).unwrap(); + let index = reader.index(); + assert_eq!(index.num_records(), 0); + assert_eq!(index.num_blocks(), 0); + } + // ==================== Default Quality Score Tests ==================== #[test] From bdcaf46445ff04159271aede3aecfb2b4af75bbd Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:26:45 -0700 Subject: [PATCH 4/5] chore(semver): bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fcbb2f8..b492d89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binseq" -version = "0.9.4" +version = "0.9.5" edition = "2024" description = "A high efficiency binary format for sequencing data" license = "MIT" From 0378f9be4bf31f39f379e15b809449754f816fa7 Mon Sep 17 00:00:00 2001 From: noamteyssier <22600644+noamteyssier@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:27:58 -0700 Subject: [PATCH 5/5] docs: update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0782fe2..98dda6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.5] - 2026-08-10 + +### Fixed + +- Support empty CBQ files in the `cbq` reader which previously failed due to index alignment error + ## [0.9.4] - 2026-07-15 ### Fixed