Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"]
Expand Down
7 changes: 7 additions & 0 deletions src/cbq/core/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ impl Index {

/// Builds the index from a byte slice
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
// 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()),
Expand Down
51 changes: 51 additions & 0 deletions src/cbq/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ impl ParallelReader for MmapReader {
}
#[cfg(test)]
mod tests {
use std::io::Write;

use tempfile::NamedTempFile;

use super::*;
use crate::BinseqRecord;

Expand Down Expand Up @@ -380,6 +384,53 @@ mod tests {
assert!(num_blocks > 0, "Should have at least one block");
}

// ==================== Empty File Tests ====================

fn make_empty_cbq() -> Vec<u8> {
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();
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 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]
Expand Down