1use crate::error::{Error, Result};
9use crate::source::ByteSource;
10
11pub const BBI_HEADER_SIZE: u64 = 64;
13pub const ZOOM_HEADER_SIZE: u64 = 24;
14pub const TOTAL_SUMMARY_SIZE: u64 = 40;
15pub const CHR_TREE_HEADER_SIZE: u64 = 32;
16pub const DATA_TREE_HEADER_SIZE: u64 = 48;
18
19pub const BIGWIG_MAGIC_SWAPPED: u32 = 0x26FC_8F88;
20pub const BIGBED_MAGIC_SWAPPED: u32 = 0xEBF2_8987;
21pub const CHR_TREE_MAGIC: u32 = 0x78CA_8C91;
22pub const CHR_TREE_MAGIC_SWAPPED: u32 = 0x91CA_8C78;
23pub const DATA_TREE_MAGIC: u32 = 0x2468_ACE0;
24pub const DATA_TREE_MAGIC_SWAPPED: u32 = 0xE0AC_6824;
25
26pub const BBI_MIN_VERSION: u16 = 3;
27pub const BBI_OUTPUT_VERSION: u16 = 4;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum BbiKind {
31 BigWig,
32 BigBed,
33}
34
35impl BbiKind {
36 pub fn as_str(self) -> &'static str {
37 match self {
38 BbiKind::BigWig => "bigwig",
39 BbiKind::BigBed => "bigbed",
40 }
41 }
42
43 pub fn is_bigbed(self) -> bool {
44 matches!(self, BbiKind::BigBed)
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct BbiHeader {
50 pub kind: BbiKind,
51 pub version: u16,
52 pub zoom_levels: u16,
53 pub chr_tree_offset: u64,
54 pub full_data_offset: u64,
55 pub full_index_offset: u64,
56 pub field_count: u16,
58 pub defined_field_count: u16,
59 pub auto_sql_offset: u64,
60 pub total_summary_offset: u64,
61 pub uncompress_buffer_size: u32,
63}
64
65#[derive(Debug, Clone, Copy)]
66pub struct ZoomHeader {
67 pub reduction_level: u32,
68 pub data_offset: u64,
69 pub index_offset: u64,
70}
71
72#[derive(Debug, Clone, Copy)]
76pub struct TotalSummary {
77 pub bases_covered: u64,
78 pub min_value: f64,
79 pub max_value: f64,
80 pub sum_data: f64,
81 pub sum_squared: f64,
82}
83
84impl Default for TotalSummary {
85 fn default() -> Self {
86 Self {
87 bases_covered: 0,
88 min_value: f64::NAN,
89 max_value: f64::NAN,
90 sum_data: 0.0,
91 sum_squared: 0.0,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy)]
97pub struct ChrTreeHeader {
98 pub block_size: u32,
99 pub key_size: u32,
100 pub val_size: u32,
101 pub item_count: u64,
102}
103
104pub fn read_header(source: &dyn ByteSource) -> Result<BbiHeader> {
109 let buf = source.read_exact_at(0, BBI_HEADER_SIZE as usize)?;
110 let path = source.path();
111 let mut c = crate::bytes::LeCursor::new(&buf, 0, path);
112
113 let magic = c.read_u32()?;
114 let kind = match magic {
115 super::BIGWIG_MAGIC => BbiKind::BigWig,
116 super::BIGBED_MAGIC => BbiKind::BigBed,
117 BIGWIG_MAGIC_SWAPPED | BIGBED_MAGIC_SWAPPED => {
118 return Err(Error::format(path, "incompatible endianness"))
119 }
120 _ => return Err(Error::format(path, "not a bigwig or bigbed file")),
121 };
122
123 let version = c.read_u16()?;
124 if version < BBI_MIN_VERSION {
125 return Err(Error::format(
126 path,
127 format!("bigwig or bigbed version {version} unsupported (>= {BBI_MIN_VERSION})"),
128 ));
129 }
130
131 Ok(BbiHeader {
132 kind,
133 version,
134 zoom_levels: c.read_u16()?,
135 chr_tree_offset: c.read_u64()?,
136 full_data_offset: c.read_u64()?,
137 full_index_offset: c.read_u64()?,
138 field_count: c.read_u16()?,
139 defined_field_count: c.read_u16()?,
140 auto_sql_offset: c.read_u64()?,
141 total_summary_offset: c.read_u64()?,
142 uncompress_buffer_size: c.read_u32()?,
143 })
147}
148
149pub fn read_zoom_headers(source: &dyn ByteSource, count: u16) -> Result<Vec<ZoomHeader>> {
150 if count == 0 {
151 return Ok(Vec::new());
152 }
153 let len = count as u64 * ZOOM_HEADER_SIZE;
154 let buf = source.read_exact_at(BBI_HEADER_SIZE, len as usize)?;
155 let mut c = crate::bytes::LeCursor::new(&buf, BBI_HEADER_SIZE, source.path());
156 let mut headers = Vec::with_capacity(count as usize);
157 for _ in 0..count {
158 let reduction_level = c.read_u32()?;
159 c.skip(4)?; headers.push(ZoomHeader {
161 reduction_level,
162 data_offset: c.read_u64()?,
163 index_offset: c.read_u64()?,
164 });
165 }
166 Ok(headers)
167}
168
169pub fn read_total_summary(source: &dyn ByteSource, offset: u64) -> Result<TotalSummary> {
171 if offset == 0 {
172 return Ok(TotalSummary::default());
173 }
174 let buf = source.read_exact_at(offset, TOTAL_SUMMARY_SIZE as usize)?;
175 let mut c = crate::bytes::LeCursor::new(&buf, offset, source.path());
176 Ok(TotalSummary {
177 bases_covered: c.read_u64()?,
178 min_value: c.read_f64()?,
179 max_value: c.read_f64()?,
180 sum_data: c.read_f64()?,
181 sum_squared: c.read_f64()?,
182 })
183}
184
185pub fn check_data_tree_magic(source: &dyn ByteSource, offset: u64) -> Result<()> {
190 let buf = source.read_exact_at(offset, 4)?;
191 let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
192 match magic {
193 DATA_TREE_MAGIC => Ok(()),
194 DATA_TREE_MAGIC_SWAPPED => Err(Error::format(
195 source.path(),
196 "incompatible endianness (data tree)",
197 )),
198 _ => Err(Error::format(
199 source.path(),
200 "invalid data tree magic number",
201 )),
202 }
203}
204
205pub const BED_FIELD_NAMES: &[&str] = &[
208 "chrom",
209 "chromStart",
210 "chromEnd",
211 "name",
212 "score",
213 "strand",
214 "thickStart",
215 "thickEnd",
216 "itemRgb",
217 "blockCount",
218 "blockSizes",
219 "blockStarts",
220];
221
222pub fn read_auto_sql(
233 source: &dyn ByteSource,
234 offset: u64,
235 field_count: u16,
236) -> Result<indexmap::IndexMap<String, String>> {
237 if offset == 0 {
238 return Ok(default_bed_fields(field_count as usize));
239 }
240 let text = read_nul_terminated(source, offset)?;
241
242 let mut fields = indexmap::IndexMap::new();
243 for line in text.lines() {
244 let Some((head, rest)) = split_once_whitespace(line.trim()) else {
247 continue;
248 };
249 let Some(list) = rest.split(';').next().filter(|l| !l.trim().is_empty()) else {
250 continue;
251 };
252 if !rest.contains(';') {
253 continue;
254 }
255 for name in list.split(',') {
256 let name = name.split_whitespace().next().unwrap_or("");
259 if !name.is_empty() {
260 fields.insert(name.to_string(), head.to_string());
261 }
262 }
263 }
264
265 validate_bed_field_names(&fields, source.path())?;
266 if fields.len() != field_count as usize {
267 return Err(Error::format(
268 source.path(),
269 format!(
270 "field count {field_count} does not match autosql field count {}",
271 fields.len()
272 ),
273 ));
274 }
275 Ok(fields)
276}
277
278fn split_once_whitespace(line: &str) -> Option<(&str, &str)> {
288 let (mut depth, mut quote) = (0i32, None::<char>);
289 let mut head_end = None;
290 for (at, c) in line.char_indices() {
291 match quote {
292 Some(q) => {
293 if c == q {
294 quote = None;
295 }
296 }
297 None => match c {
298 '"' | '\'' => quote = Some(c),
299 '(' | '[' | '{' => depth += 1,
300 ')' | ']' | '}' => depth -= 1,
301 _ if depth <= 0 && c.is_whitespace() => {
302 head_end = Some(at);
303 break;
304 }
305 _ => {}
306 },
307 }
308 }
309 let (head, rest) = line.split_at(head_end?);
310 Some((head, rest.trim_start()))
311}
312
313fn read_nul_terminated(source: &dyn ByteSource, offset: u64) -> Result<String> {
316 const CHUNK: usize = 4096;
317 const MAX: usize = 1 << 20;
320 let mut buf: Vec<u8> = Vec::new();
321 loop {
322 let chunk = source.read_at(offset + buf.len() as u64, CHUNK)?;
323 if chunk.is_empty() {
324 return Err(Error::corrupt(
325 source.path(),
326 offset,
327 "autosql block is not NUL-terminated before the end of the file",
328 ));
329 }
330 if let Some(at) = memchr::memchr(0, &chunk) {
331 buf.extend_from_slice(&chunk[..at]);
332 break;
333 }
334 buf.extend_from_slice(&chunk);
335 if buf.len() > MAX {
336 return Err(Error::corrupt(
337 source.path(),
338 offset,
339 format!("autosql block is longer than {MAX} bytes"),
340 ));
341 }
342 }
343 String::from_utf8(buf)
344 .map_err(|_| Error::corrupt(source.path(), offset, "autosql is not valid UTF-8"))
345}
346
347pub fn validate_bed_field_names(
360 fields: &indexmap::IndexMap<String, String>,
361 path: &str,
362) -> Result<()> {
363 let bad = || Error::format(path, "missing or misplaced chr, start or end in autosql");
364 if fields.len() < 3 {
365 return Err(bad());
366 }
367 let name = |i: usize| fields.get_index(i).map(|(k, _)| k.as_str()).unwrap_or("");
368 if !is_chr_name(name(0)) || !is_coord_name(name(1), "start") || !is_coord_name(name(2), "end") {
369 return Err(bad());
370 }
371 Ok(())
372}
373
374fn is_chr_name(name: &str) -> bool {
377 let lower = name.to_ascii_lowercase();
378 let rest = match strip_chr_prefix(&lower) {
379 Some(rest) => rest,
380 None => return false,
381 };
382 let rest = rest.strip_prefix('_').unwrap_or(rest);
383 matches!(rest, "" | "id" | "name")
384}
385
386fn is_coord_name(name: &str, suffix: &str) -> bool {
389 let lower = name.to_ascii_lowercase();
390 if lower == suffix {
391 return true;
392 }
393 let Some(rest) = strip_chr_prefix(&lower) else {
394 return false;
395 };
396 rest.strip_prefix('_').unwrap_or(rest) == suffix
397}
398
399fn strip_chr_prefix(lower: &str) -> Option<&str> {
400 let rest = lower.strip_prefix("chr")?;
401 Some(rest.strip_prefix("om").unwrap_or(rest))
402}
403
404pub fn default_bed_fields(col_count: usize) -> indexmap::IndexMap<String, String> {
407 (0..col_count)
408 .map(|i| {
409 let name = BED_FIELD_NAMES
410 .get(i)
411 .map(|n| (*n).to_string())
412 .unwrap_or_else(|| format!("field{}", i + 1));
413 (name, "string".to_string())
414 })
415 .collect()
416}
417
418pub fn to_bbi_u32(value: i64, what: &str) -> Result<u32> {
425 if !(0..=0xFFFF_FFFF).contains(&value) {
426 return Err(Error::invalid(format!(
427 "{what} {value} does not fit the 32 bits a bigwig or bigbed file stores it on"
428 )));
429 }
430 Ok(value as u32)
431}
432
433pub fn to_bbi_f32(value: f64) -> f32 {
440 const LIMIT: f64 = f32::MAX as f64;
441 if value > LIMIT {
442 return f32::MAX;
443 }
444 if value < -LIMIT {
445 return f32::MIN;
446 }
447 value as f32
448}
449
450pub fn write_header(header: &BbiHeader) -> Result<Vec<u8>> {
457 let mut out = Vec::with_capacity(BBI_HEADER_SIZE as usize);
458 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&header.version.to_le_bytes());
460 out.extend_from_slice(&header.zoom_levels.to_le_bytes());
461 out.extend_from_slice(&header.chr_tree_offset.to_le_bytes());
462 out.extend_from_slice(&header.full_data_offset.to_le_bytes());
463 out.extend_from_slice(&header.full_index_offset.to_le_bytes());
464 out.extend_from_slice(&header.field_count.to_le_bytes());
465 out.extend_from_slice(&header.defined_field_count.to_le_bytes());
466 out.extend_from_slice(&header.auto_sql_offset.to_le_bytes());
467 out.extend_from_slice(&header.total_summary_offset.to_le_bytes());
468 out.extend_from_slice(&header.uncompress_buffer_size.to_le_bytes());
469 out.extend_from_slice(&[0u8; 8]); debug_assert_eq!(out.len(), BBI_HEADER_SIZE as usize);
471 Ok(out)
472}
473
474pub fn write_zoom_header(header: &ZoomHeader) -> Vec<u8> {
476 let mut out = Vec::with_capacity(ZOOM_HEADER_SIZE as usize);
477 out.extend_from_slice(&header.reduction_level.to_le_bytes());
478 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&header.data_offset.to_le_bytes());
480 out.extend_from_slice(&header.index_offset.to_le_bytes());
481 out
482}
483
484pub fn write_total_summary(summary: &TotalSummary) -> Vec<u8> {
490 let empty = summary.bases_covered == 0;
491 let mut out = Vec::with_capacity(TOTAL_SUMMARY_SIZE as usize);
492 out.extend_from_slice(&summary.bases_covered.to_le_bytes());
493 out.extend_from_slice(&if empty { 0.0 } else { summary.min_value }.to_le_bytes());
494 out.extend_from_slice(&if empty { 0.0 } else { summary.max_value }.to_le_bytes());
495 out.extend_from_slice(&summary.sum_data.to_le_bytes());
496 out.extend_from_slice(&summary.sum_squared.to_le_bytes());
497 out
498}
499
500pub const BED_FIELD_TYPES: &[&str] = &["string", "int", "uint", "float"];
502
503#[derive(Debug, Clone)]
505pub struct BedAutoSql {
506 pub text: String,
507 pub field_count: u16,
508 pub defined_field_count: u16,
511}
512
513const BED_FIELD_COMMENTS: &[&str] = &[
518 "Reference sequence chromosome or scaffold",
519 "Start position in chromosome",
520 "End position in chromosome",
521 "Name of item",
522 "Score from 0-1000",
523 "+ or - for strand",
524 "Start of where display should be thick",
525 "End of where display should be thick",
526 "Used as itemRgb as of 2004-11-22",
527 "Number of blocks",
528 "Comma separated list of block sizes",
529 "Start positions relative to chromStart",
530];
531
532pub fn build_auto_sql(fields: &indexmap::IndexMap<String, String>) -> Result<BedAutoSql> {
539 validate_bed_field_names(fields, "fields")?;
540 for (name, kind) in fields {
541 if !BED_FIELD_TYPES.contains(&kind.as_str()) {
542 return Err(Error::invalid(format!(
543 "field type {kind} of {name} invalid (string, int, uint, float)"
544 )));
545 }
546 if let Some(bad) = name
552 .chars()
553 .find(|c| c.is_whitespace() || matches!(c, ';' | ',' | '(' | ')' | '"'))
554 {
555 return Err(Error::invalid(format!(
556 "field name {name:?} contains {bad:?}, which autosql uses as punctuation (names may not hold whitespace, ';', ',', parentheses or quotes)"
557 )));
558 }
559 if name.is_empty() {
560 return Err(Error::invalid("a field name may not be empty"));
561 }
562 }
563
564 let named = |index: usize, given: &str| -> String {
565 if index < 3 {
566 BED_FIELD_NAMES[index].to_string()
567 } else {
568 given.to_string()
569 }
570 };
571
572 let mut text = String::from("table gwseqBed\n\"gwseq_io bed entries\"\n(\n");
573 for (index, (given, kind)) in fields.iter().enumerate() {
574 let name = named(index, given);
575 let standard =
576 index < 3 || (index < BED_FIELD_NAMES.len() && given == BED_FIELD_NAMES[index]);
577 let kind = if index < 3 {
578 if index == 0 {
579 "string"
580 } else {
581 "uint"
582 }
583 } else {
584 kind.as_str()
585 };
586 let comment = if standard {
587 BED_FIELD_COMMENTS[index]
588 } else {
589 &name
590 };
591 text.push_str(&format!("{kind} {name};\t\"{comment}\"\n"));
592 }
593 text.push_str(")\n");
594
595 let mut defined_field_count = 0u16;
596 for (index, (given, _)) in fields.iter().enumerate() {
597 let name = named(index, given);
598 if index >= BED_FIELD_NAMES.len() || name != BED_FIELD_NAMES[index] {
599 break;
600 }
601 defined_field_count += 1;
602 }
603
604 Ok(BedAutoSql {
605 text,
606 field_count: to_bbi_u32(fields.len() as i64, "fieldCount")? as u16,
607 defined_field_count,
608 })
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::source::testing::MemorySource;
615
616 fn header_bytes(magic: u32, version: u16, zoom_levels: u16) -> Vec<u8> {
618 let mut b = Vec::new();
619 b.extend_from_slice(&magic.to_le_bytes());
620 b.extend_from_slice(&version.to_le_bytes());
621 b.extend_from_slice(&zoom_levels.to_le_bytes());
622 b.extend_from_slice(&1000u64.to_le_bytes()); b.extend_from_slice(&2000u64.to_le_bytes()); b.extend_from_slice(&3000u64.to_le_bytes()); b.extend_from_slice(&9u16.to_le_bytes()); b.extend_from_slice(&6u16.to_le_bytes()); b.extend_from_slice(&400u64.to_le_bytes()); b.extend_from_slice(&500u64.to_le_bytes()); b.extend_from_slice(&32768u32.to_le_bytes()); b.extend_from_slice(&0u64.to_le_bytes()); assert_eq!(b.len(), 64);
632 b
633 }
634
635 #[test]
636 fn reads_a_bigwig_header() {
637 let source = MemorySource::new(header_bytes(super::super::BIGWIG_MAGIC, 4, 0));
638 let h = read_header(&source).unwrap();
639 assert_eq!(h.kind, BbiKind::BigWig);
640 assert_eq!(h.version, 4);
641 assert_eq!(h.chr_tree_offset, 1000);
642 assert_eq!(h.full_index_offset, 3000);
643 assert_eq!(h.field_count, 9);
644 assert_eq!(h.uncompress_buffer_size, 32768);
645 }
646
647 #[test]
648 fn a_bigbed_magic_gives_the_other_kind() {
649 let source = MemorySource::new(header_bytes(super::super::BIGBED_MAGIC, 4, 0));
650 assert_eq!(read_header(&source).unwrap().kind, BbiKind::BigBed);
651 }
652
653 #[test]
654 fn a_byte_swapped_file_is_refused_by_name() {
655 for magic in [BIGWIG_MAGIC_SWAPPED, BIGBED_MAGIC_SWAPPED] {
656 let source = MemorySource::new(header_bytes(magic, 4, 0));
657 let err = read_header(&source).unwrap_err().to_string();
658 assert!(err.contains("incompatible endianness"), "{err}");
659 }
660 }
661
662 #[test]
663 fn something_else_entirely_is_not_a_bbi_file() {
664 let source = MemorySource::new(header_bytes(0xDEAD_BEEF, 4, 0));
665 let err = read_header(&source).unwrap_err().to_string();
666 assert!(err.contains("not a bigwig or bigbed file"), "{err}");
667 }
668
669 #[test]
670 fn an_old_version_names_the_floor() {
671 let source = MemorySource::new(header_bytes(super::super::BIGWIG_MAGIC, 2, 0));
672 let err = read_header(&source).unwrap_err().to_string();
673 assert!(err.contains("version 2 unsupported (>= 3)"), "{err}");
674 }
675
676 #[test]
677 fn a_truncated_header_is_corrupt_not_a_panic() {
678 let source = MemorySource::new(vec![0u8; 40]);
679 assert!(matches!(
680 read_header(&source),
681 Err(crate::error::Error::Corrupt { .. })
682 ));
683 }
684
685 #[test]
686 fn zoom_headers_follow_the_common_one() {
687 let mut b = header_bytes(super::super::BIGWIG_MAGIC, 4, 2);
688 for (reduction, data, index) in [(10u32, 100u64, 200u64), (40, 300, 400)] {
689 b.extend_from_slice(&reduction.to_le_bytes());
690 b.extend_from_slice(&0u32.to_le_bytes());
691 b.extend_from_slice(&data.to_le_bytes());
692 b.extend_from_slice(&index.to_le_bytes());
693 }
694 let source = MemorySource::new(b);
695 let zooms = read_zoom_headers(&source, 2).unwrap();
696 assert_eq!(zooms.len(), 2);
697 assert_eq!(zooms[0].reduction_level, 10);
698 assert_eq!(zooms[1].index_offset, 400);
699 assert!(read_zoom_headers(&source, 0).unwrap().is_empty());
700 }
701
702 #[test]
703 fn a_summary_offset_of_zero_means_the_file_carries_none() {
704 let source = MemorySource::new(vec![0u8; 8]);
705 let s = read_total_summary(&source, 0).unwrap();
706 assert_eq!(s.bases_covered, 0);
707 assert!(s.min_value.is_nan() && s.max_value.is_nan());
708 }
709
710 #[test]
711 fn reads_a_total_summary() {
712 let mut b = vec![0u8; 64];
713 b.extend_from_slice(&1234u64.to_le_bytes());
714 b.extend_from_slice(&(-1.5f64).to_le_bytes());
715 b.extend_from_slice(&9.25f64.to_le_bytes());
716 b.extend_from_slice(&100.0f64.to_le_bytes());
717 b.extend_from_slice(&500.0f64.to_le_bytes());
718 let source = MemorySource::new(b);
719 let s = read_total_summary(&source, 64).unwrap();
720 assert_eq!(s.bases_covered, 1234);
721 assert_eq!((s.min_value, s.max_value), (-1.5, 9.25));
722 assert_eq!((s.sum_data, s.sum_squared), (100.0, 500.0));
723 }
724
725 #[test]
726 fn the_data_tree_magic_is_checked_both_ways() {
727 let source = MemorySource::new(DATA_TREE_MAGIC.to_le_bytes().to_vec());
728 assert!(check_data_tree_magic(&source, 0).is_ok());
729 let source = MemorySource::new(DATA_TREE_MAGIC_SWAPPED.to_le_bytes().to_vec());
730 let err = check_data_tree_magic(&source, 0).unwrap_err().to_string();
731 assert!(err.contains("incompatible endianness"), "{err}");
732 let source = MemorySource::new(vec![1, 2, 3, 4]);
733 assert!(check_data_tree_magic(&source, 0).is_err());
734 }
735}
736
737#[cfg(test)]
738mod write_tests {
739 use super::*;
740 use crate::source::testing::MemorySource;
741
742 fn fields(pairs: &[(&str, &str)]) -> indexmap::IndexMap<String, String> {
743 pairs
744 .iter()
745 .map(|(a, b)| ((*a).to_string(), (*b).to_string()))
746 .collect()
747 }
748
749 #[test]
750 fn a_written_header_reads_back_as_itself() {
751 let header = BbiHeader {
752 kind: BbiKind::BigBed,
753 version: BBI_OUTPUT_VERSION,
754 zoom_levels: 7,
755 chr_tree_offset: 4096,
756 full_data_offset: 512,
757 full_index_offset: 2048,
758 field_count: 6,
759 defined_field_count: 4,
760 auto_sql_offset: 368,
761 total_summary_offset: 400,
762 uncompress_buffer_size: 32768,
763 };
764 let mut bytes = write_header(&header).unwrap();
765 let err = read_header(&MemorySource::new(bytes.clone())).unwrap_err();
767 assert!(err.to_string().contains("not a bigwig or bigbed"), "{err}");
768
769 bytes[..4].copy_from_slice(&super::super::BIGBED_MAGIC.to_le_bytes());
770 let read = read_header(&MemorySource::new(bytes)).unwrap();
771 assert_eq!(read.kind, BbiKind::BigBed);
772 assert_eq!(read.version, header.version);
773 assert_eq!(read.zoom_levels, header.zoom_levels);
774 assert_eq!(read.chr_tree_offset, header.chr_tree_offset);
775 assert_eq!(read.full_data_offset, header.full_data_offset);
776 assert_eq!(read.full_index_offset, header.full_index_offset);
777 assert_eq!(read.field_count, header.field_count);
778 assert_eq!(read.defined_field_count, header.defined_field_count);
779 assert_eq!(read.auto_sql_offset, header.auto_sql_offset);
780 assert_eq!(read.total_summary_offset, header.total_summary_offset);
781 assert_eq!(read.uncompress_buffer_size, header.uncompress_buffer_size);
782 }
783
784 #[test]
785 fn a_written_zoom_header_and_summary_read_back_as_themselves() {
786 let zooms = [
787 ZoomHeader {
788 reduction_level: 10,
789 data_offset: 100,
790 index_offset: 200,
791 },
792 ZoomHeader {
793 reduction_level: 40,
794 data_offset: 300,
795 index_offset: 400,
796 },
797 ];
798 let mut bytes = vec![0u8; BBI_HEADER_SIZE as usize];
799 for z in &zooms {
800 bytes.extend_from_slice(&write_zoom_header(z));
801 }
802 let read = read_zoom_headers(&MemorySource::new(bytes), 2).unwrap();
803 assert_eq!(read.len(), 2);
804 assert_eq!(read[1].reduction_level, 40);
805 assert_eq!(read[1].data_offset, 300);
806 assert_eq!(read[1].index_offset, 400);
807
808 let summary = TotalSummary {
809 bases_covered: 1234,
810 min_value: -1.5,
811 max_value: 9.25,
812 sum_data: 1000.0,
813 sum_squared: 5000.0,
814 };
815 let read = read_total_summary(&MemorySource::new(write_total_summary(&summary)), 0);
816 assert_eq!(read.unwrap().bases_covered, 0);
818 let mut padded = vec![0u8];
819 padded.extend_from_slice(&write_total_summary(&summary));
820 let read = read_total_summary(&MemorySource::new(padded), 1).unwrap();
821 assert_eq!(read.bases_covered, 1234);
822 assert_eq!(read.min_value, -1.5);
823 assert_eq!(read.sum_squared, 5000.0);
824 }
825
826 #[test]
827 fn an_empty_summary_writes_zero_extremes_rather_than_nan() {
828 let bytes = write_total_summary(&TotalSummary::default());
829 let mut padded = vec![0u8];
830 padded.extend_from_slice(&bytes);
831 let read = read_total_summary(&MemorySource::new(padded), 1).unwrap();
832 assert_eq!(read.min_value, 0.0);
833 assert_eq!(read.max_value, 0.0);
834 }
835
836 #[test]
837 fn built_autosql_reads_back_as_the_fields_it_describes() {
838 let declared = fields(&[
841 ("chr", "string"),
842 ("start", "uint"),
843 ("end", "uint"),
844 ("name", "string"),
845 ("score", "uint"),
846 ]);
847 let sql = build_auto_sql(&declared).unwrap();
848 assert_eq!(sql.field_count, 5);
849 assert_eq!(sql.defined_field_count, 5);
851
852 let mut bytes = vec![0u8];
853 bytes.extend_from_slice(sql.text.as_bytes());
854 bytes.push(0);
855 let read = read_auto_sql(&MemorySource::new(bytes), 1, 5).unwrap();
856 assert_eq!(
857 read.keys().map(String::as_str).collect::<Vec<_>>(),
858 ["chrom", "chromStart", "chromEnd", "name", "score"]
859 );
860 assert_eq!(read["chrom"], "string");
861 assert_eq!(read["score"], "uint");
862 }
863
864 #[test]
865 fn a_non_standard_column_stops_the_defined_field_count() {
866 let declared = fields(&[
867 ("chr", "string"),
868 ("start", "uint"),
869 ("end", "uint"),
870 ("pvalue", "float"),
871 ("score", "uint"),
872 ]);
873 let sql = build_auto_sql(&declared).unwrap();
874 assert_eq!(sql.defined_field_count, 3);
876 assert_eq!(sql.field_count, 5);
877 assert!(
879 sql.text.contains("float pvalue;\t\"pvalue\"\n"),
880 "{}",
881 sql.text
882 );
883 }
884
885 #[test]
886 fn a_bad_field_name_or_type_is_refused_before_anything_is_written() {
887 let err = build_auto_sql(&fields(&[
888 ("name", "string"),
889 ("start", "uint"),
890 ("end", "uint"),
891 ]))
892 .unwrap_err()
893 .to_string();
894 assert!(err.contains("missing or misplaced"), "{err}");
895 let err = build_auto_sql(&fields(&[
896 ("chr", "string"),
897 ("start", "uint"),
898 ("end", "uint"),
899 ("x", "double"),
900 ]))
901 .unwrap_err()
902 .to_string();
903 assert!(err.contains("field type double"), "{err}");
904 }
905
906 #[test]
907 fn a_coordinate_past_32_bits_is_refused_rather_than_truncated() {
908 assert_eq!(to_bbi_u32(4_294_967_295, "chromEnd").unwrap(), u32::MAX);
909 let err = to_bbi_u32(4_294_967_296, "chromEnd")
910 .unwrap_err()
911 .to_string();
912 assert!(err.contains("chromEnd 4294967296"), "{err}");
913 assert!(to_bbi_u32(-1, "chromStart").is_err());
914 }
915
916 #[test]
917 fn narrowing_a_summary_clamps_rather_than_reaching_infinity() {
918 assert_eq!(to_bbi_f32(1.0e300), f32::MAX);
919 assert_eq!(to_bbi_f32(-1.0e300), f32::MIN);
920 assert_eq!(to_bbi_f32(1.5), 1.5f32);
921 }
922}
923
924#[cfg(test)]
925mod autosql_tests {
926 use super::*;
927
928 fn fields(pairs: &[(&str, &str)]) -> indexmap::IndexMap<String, String> {
929 pairs
930 .iter()
931 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
932 .collect()
933 }
934
935 #[test]
940 fn a_type_with_spaces_inside_its_brackets_is_one_token() {
941 for (line, kind, rest) in [
942 (
943 "enum(\"+\", \"-\", \".\") strand;",
944 "enum(\"+\", \"-\", \".\")",
945 "strand;",
946 ),
947 ("set(a, b) flags;", "set(a, b)", "flags;"),
948 (
949 "int[blockCount] blockSizes;",
950 "int[blockCount]",
951 "blockSizes;",
952 ),
953 ("uint score;", "uint", "score;"),
954 ("string name; comment", "string", "name; comment"),
955 ] {
956 assert_eq!(split_once_whitespace(line), Some((kind, rest)), "{line}");
957 }
958 assert_eq!(split_once_whitespace("uint"), None);
960 }
961
962 #[test]
966 fn a_field_name_may_not_hold_autosql_punctuation() {
967 for bad in [
968 "two words",
969 "semi;colon",
970 "com,ma",
971 "paren(s)",
972 "quo\"te",
973 "",
974 ] {
975 let f = fields(&[
976 ("chr", "string"),
977 ("start", "uint"),
978 ("end", "uint"),
979 (bad, "string"),
980 ]);
981 assert!(
982 build_auto_sql(&f).is_err(),
983 "{bad:?} was accepted as a field name"
984 );
985 }
986 let ok = fields(&[
987 ("chr", "string"),
988 ("start", "uint"),
989 ("end", "uint"),
990 ("itemRgb", "string"),
991 ]);
992 assert!(build_auto_sql(&ok).is_ok());
993 }
994}