gwseq-io 0.2.0

Rust library for processing bigWig, bigBed, BAM and HiC files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
//! Every parser, given bytes it did not expect.
//!
//! In `src/` rather than `tests/` on purpose: most of these parsers are private
//! — `bbi::block`, `bbi::chr_tree`, `bam::record` — and a fuzz harness is not
//! a reason to widen the public API. An integration test would have to.
//!
//! The invariant is one sentence: **a parser returns `Err`, it never panics and
//! it never hangs.** Not "it rejects bad input" — a parser is free to accept
//! nonsense and hand back nonsense — but that a caller who feeds it a corrupt
//! or truncated or hostile file gets a `Result` back rather than an abort.
//! Checked from the inside, where a failure names the parser rather than a
//! process. The outside version — mutate a real file, assert no child dies of
//! a signal — is what `fuzz/` is for.
//!
//! Deliberately *not* `cargo-fuzz`. That needs a nightly toolchain, a corpus
//! and a machine to run it on; the targets for it live in `fuzz/` and are worth
//! having, but they run when someone remembers. This runs on stable, in a
//! second, on every `cargo test`, and it is the one that will actually catch a
//! regression. The generator is seeded, so a failure reproduces exactly.
//!
//! Three shapes of input, because they find different things:
//!
//! 1. **Random bytes.** Finds the parser that indexes before it checks.
//! 2. **Structured-then-corrupted.** A header this crate wrote, with bytes
//!    flipped. Finds the parser that trusts a count or an offset — a length
//!    field is only interesting once it is *nearly* right.
//! 3. **Truncations.** Every prefix of a valid header. Finds the parser that
//!    reads past the end after checking only the first field.

use bytes::Bytes;

use crate::source::testing::MemorySource;

/// A tiny deterministic generator. `SmallRng` is behind a feature and this
/// needs sixty lines of a PRNG, not a dependency.
struct Rng(u64);

impl Rng {
    fn new(seed: u64) -> Self {
        // Any non-zero state will do; the constant is only there so a seed of 0
        // is not a fixed point.
        Self(seed ^ 0x9E37_79B9_7F4A_7C15)
    }

    fn next(&mut self) -> u64 {
        // xorshift64*, which is short, has a long enough period for this, and
        // gives the same stream on every platform.
        let mut x = self.0;
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        self.0 = x;
        x.wrapping_mul(0x2545_F491_4F6C_DD1D)
    }

    fn below(&mut self, bound: usize) -> usize {
        if bound == 0 {
            return 0;
        }
        (self.next() % bound as u64) as usize
    }

    fn bytes(&mut self, len: usize) -> Vec<u8> {
        (0..len).map(|_| (self.next() >> 24) as u8).collect()
    }
}

/// How the harness hands bytes to a parser that reads through a source.
///
/// Every input is run through both. `Bare` is the source the parser sees in a
/// unit test; `Cached` is the one it sees in a real reader, since every open
/// file in this crate is wrapped in a [`CachedSource`]. The difference is not
/// cosmetic: `MemorySource` clamps a length where it reads it, so a harness
/// that only ever used it could not see a layer above it sizing a buffer from
/// that same number first — which is exactly what the cache used to do.
///
/// The block size and count are small so that a few hundred bytes of fuzz input
/// still spans several blocks and exercises the multi-block path.
#[derive(Clone, Copy, Debug)]
enum Layer {
    Bare,
    Cached,
}

impl Layer {
    fn source(self, bytes: &[u8]) -> Box<dyn crate::source::ByteSource> {
        let memory = MemorySource::new(bytes.to_vec());
        match self {
            Layer::Bare => Box::new(memory),
            Layer::Cached => Box::new(crate::source::CachedSource::new(memory, 32, 4)),
        }
    }
}

/// One parser, taking bytes and doing whatever it does with them.
type Parser = fn(&[u8], Layer);

/// Every parser this crate exposes over a byte source, as one closure each.
///
/// Named, so a failure says which. The bodies ignore the `Result`: what is
/// under test is that there *is* one.
fn parsers() -> Vec<(&'static str, Parser)> {
    vec![
        ("sniff", |b, layer| {
            let _ = crate::sniff_source(layer.source(b).as_ref());
        }),
        ("bbi::header", |b, layer| {
            let _ = crate::bbi::header::read_header(layer.source(b).as_ref());
        }),
        ("bbi::zoom_headers", |b, layer| {
            // A count of 10 is the header's own maximum, so this asks for more
            // than any well-formed prefix holds.
            let _ = crate::bbi::header::read_zoom_headers(layer.source(b).as_ref(), 10);
        }),
        ("bbi::total_summary", |b, layer| {
            let _ = crate::bbi::header::read_total_summary(layer.source(b).as_ref(), 1);
        }),
        ("bbi::auto_sql", |b, layer| {
            let _ = crate::bbi::header::read_auto_sql(layer.source(b).as_ref(), 1, 6);
        }),
        ("bbi::chr_tree", |b, layer| {
            let _ = crate::bbi::chr_tree::read(layer.source(b).as_ref(), 0);
        }),
        ("bbi::wig_header", |b, _| {
            let _ = crate::bbi::block::read_wig_header(b, "fuzz.bigwig");
        }),
        ("bbi::wig_items", |b, _| {
            // The header is read from the same bytes, so a corrupt item count
            // is walked with whatever encoding those bytes name.
            if let Ok(header) = crate::bbi::block::read_wig_header(b, "fuzz.bigwig") {
                for i in 0..(header.item_count as usize).min(4096) {
                    let _ = crate::bbi::block::read_wig_item(b, &header, i, "fuzz.bigwig");
                }
            }
        }),
        ("bbi::decompress", |b, _| {
            // A `uncompress_buffer_size` from the bytes themselves, which is
            // what a corrupt header supplies.
            let hint = u32::from_le_bytes([
                b.first().copied().unwrap_or(0),
                b.get(1).copied().unwrap_or(0),
                b.get(2).copied().unwrap_or(0),
                b.get(3).copied().unwrap_or(0),
            ]);
            let _ = crate::bbi::block::decompress(Bytes::from(b.to_vec()), hint, "fuzz.bigwig");
        }),
        ("bbi::bed_records", |b, _| {
            let _ = crate::bbi::block::visit_bed_records(b, "fuzz.bigbed", |_, _, _| {});
        }),
        ("bam::header", |b, layer| {
            let _ = crate::bam::header::read(layer.source(b).as_ref());
        }),
        ("bam::index", |b, layer| {
            let _ = crate::bam::bai::BamIndex::read(layer.source(b).as_ref());
        }),
        ("bam::records", |b, _| {
            let names = std::sync::Arc::new(vec!["chr1".to_string(), "chr2".to_string()]);
            let filter = crate::bam::EntryFilter {
                chr_index: None,
                start: 0,
                end: None,
                standard_flags: false,
            };
            let _ = crate::bam::record::decode_block(
                &Bytes::from(b.to_vec()),
                true,
                &filter,
                &names,
                "fuzz.bam",
            );
        }),
        ("hic::header", |b, layer| {
            let _ = crate::hic::header::read_header(layer.source(b).as_ref());
        }),
        // The five below were the gap between "every parser is fuzzed" and what
        // was actually fuzzed. Each needs a little scaffolding to reach — a
        // batch of loci, a chunk, a master-index item, a record context, a
        // footer entry — which is why they were skipped, and why they were
        // exactly the ones worth adding.
        ("bbi::rtree_walk", |b, layer| {
            let source = layer.source(b);
            let locs = [crate::genomic::IndexedLoc {
                chr_index: 0,
                start: 0,
                end: i64::MAX / 2,
                binned_start: 0,
                binned_end: i64::MAX / 2,
                bin_size: 1.0,
                reverse: false,
                output_start: 0,
                output_end: 1,
            }];
            let batch = crate::genomic::LocBatch { start: 0, end: 1 };
            let tracker = crate::progress::ProgressTracker::with_callback(0, None);
            // The root is at 0, so the bytes are read as a node whatever they
            // are; the walk then follows whatever offsets they name.
            if let Ok(walk) =
                crate::bbi::rtree::LeafWalk::new(source.as_ref(), 0, &locs, batch, &tracker)
            {
                for leaf in walk.take(64) {
                    if leaf.is_err() {
                        break;
                    }
                }
            }
        }),
        ("bam::bgzf_chunk", |b, layer| {
            let source = layer.source(b);
            let chunk = crate::bam::Chunk {
                begin: crate::bam::VirtualOffset::new(0, 0),
                end: crate::bam::VirtualOffset::new(b.len() as u64, 0),
            };
            let _ = crate::bam::bgzf::decompress_chunk(source.as_ref(), chunk, "fuzz.bam");
        }),
        ("hic::matrix_metadata", |b, layer| {
            let source = layer.source(b);
            let item = crate::hic::HiCIndexItem {
                position: 0,
                size: b.len() as i64,
            };
            let _ = crate::hic::matrix::read_matrix_metadata(source.as_ref(), item, 0, 0);
        }),
        ("hic::block", |b, _| {
            let chr = crate::genomic::ChrEntry {
                id: "chr1".to_string(),
                size: 1_000_000,
                index: 0,
            };
            let side = crate::hic::matrix::Side {
                chr: chr.clone(),
                start: 0,
                end: 1_000_000,
                binned_start: 0,
                binned_end: 1_000_000,
                bin_start: 0,
                bin_end: 100,
            };
            let loc = crate::hic::matrix::Loc2D {
                x: side.clone(),
                y: side,
                bin_size: 10_000,
                reversed: false,
            };
            let vectors = crate::hic::Normalizations::default();
            let ctx = crate::hic::block::RecordContext {
                loc: &loc,
                normalization: "NONE",
                mode: crate::hic::HiCMode::Observed,
                vectors: &vectors,
                average_value: 1.0,
                min_distance: None,
                max_distance: None,
            };
            let item = crate::hic::HiCIndexItem {
                position: 0,
                size: b.len() as i64,
            };
            // Both layouts, since the version decides which one the bytes are
            // read as and they disagree about nearly every field.
            for version in [8i64, 9] {
                let _ = crate::hic::block::read_block(
                    Bytes::from(b.to_vec()),
                    version,
                    item,
                    &ctx,
                    "fuzz.hic",
                );
            }
        }),
        ("hic::normalization_vector", |b, layer| {
            let source = layer.source(b);
            let mut footer = crate::hic::HiCFooter::default();
            let vector = crate::hic::header::NormalizationVector {
                normalization: "KR".to_string(),
                chr_index: 0,
                unit: "bp".to_string(),
                bin_size: 10_000,
                position: 0,
                byte_count: b.len() as i64,
            };
            footer.normalization_vectors.insert(
                crate::hic::header::vector_key("KR", 10_000, "bp", Some(0)),
                vector,
            );
            for version in [8i64, 9] {
                let _ = crate::hic::header::read_normalization_vector(
                    source.as_ref(),
                    &footer,
                    version,
                    0,
                    "bp",
                    10_000,
                    "KR",
                );
            }
        }),
        ("hic::footer", |b, layer| {
            // A header the footer can be read against: whatever these bytes
            // parse as, or a plausible one when they parse as nothing.
            let source = layer.source(b);
            if let Ok(header) = crate::hic::header::read_header(source.as_ref()) {
                let _ = crate::hic::header::read_footer(source.as_ref(), &header);
            }
        }),
    ]
}

/// Run every parser over `input`, catching a panic so the failure names the
/// parser and the input rather than aborting the test binary.
fn check(label: &str, input: &[u8]) {
    for (name, parse) in parsers() {
        for layer in [Layer::Bare, Layer::Cached] {
            // `catch_unwind` is what turns "the test binary died" into "this
            // parser, on this input". The parsers hold no state across calls,
            // so there is nothing to be left inconsistent by unwinding out of
            // one.
            let result = std::panic::catch_unwind(|| parse(input, layer));
            if result.is_err() {
                // Printed rather than left to the assertion: the panic hook is
                // silenced for the whole run — thousands of panics are the
                // point — and that silence swallows the assertion's own message
                // too, which used to leave a failure saying only which test.
                eprintln!(
                    "{name} panicked on {label} through {layer:?} ({} bytes): {}",
                    input.len(),
                    hex(input)
                );
            }
            assert!(
                result.is_ok(),
                "{name} panicked on {label} through {layer:?} ({} bytes): {}",
                input.len(),
                hex(input)
            );
        }
    }
}

/// The input, short enough to paste into a test.
fn hex(input: &[u8]) -> String {
    let head: String = input.iter().take(64).map(|b| format!("{b:02x}")).collect();
    if input.len() > 64 {
        format!("{head}... ({} bytes)", input.len())
    } else {
        head
    }
}

/// Quiet the panic hook for the duration: a caught panic still prints its
/// message and a backtrace, and this test causes thousands on purpose.
fn without_panic_output<R>(f: impl FnOnce() -> R) -> R {
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(|_| {}));
    let out = f();
    std::panic::set_hook(previous);
    out
}

#[test]
fn no_parser_panics_on_random_bytes() {
    without_panic_output(|| {
        let mut rng = Rng::new(1);
        for _ in 0..3000 {
            // Sizes around the header boundaries, where an off-by-one lives:
            // nothing, a few bytes, just under and just over 64.
            let len = match rng.below(4) {
                0 => rng.below(8),
                1 => 60 + rng.below(10),
                2 => rng.below(256),
                _ => rng.below(4096),
            };
            let input = rng.bytes(len);
            check("random bytes", &input);
        }
    });
}

/// Headers this crate wrote, with bytes flipped.
///
/// The interesting case: a length or an offset that is *nearly* right is what
/// gets a parser past its magic check and into the code that trusts it.
#[test]
fn no_parser_panics_on_a_corrupted_header() {
    let seeds = valid_headers();
    without_panic_output(|| {
        let mut rng = Rng::new(2);
        for (label, seed) in &seeds {
            for _ in 0..600 {
                let mut input = seed.clone();
                if input.is_empty() {
                    continue;
                }
                for _ in 0..1 + rng.below(6) {
                    let at = rng.below(input.len());
                    input[at] = (rng.next() >> 24) as u8;
                }
                check(label, &input);
            }
        }
    });
}

#[test]
fn no_parser_panics_on_a_truncated_header() {
    let seeds = valid_headers();
    without_panic_output(|| {
        for (label, seed) in &seeds {
            // Every prefix, not a sample: these are a few hundred bytes each,
            // and the one that matters is always the one nobody guessed.
            for len in 0..seed.len() {
                check(label, &seed[..len]);
            }
        }
    });
}

/// Well-formed headers of each format, to corrupt and to truncate.
///
/// Built here rather than read from a fixture, so the test runs on a machine
/// that has none — which is most of them.
fn valid_headers() -> Vec<(&'static str, Vec<u8>)> {
    let mut out = Vec::new();

    // A bbi header, its zoom slots, a summary and a chromosome tree.
    let mut bbi = Vec::new();
    bbi.extend_from_slice(&0x888F_FC26u32.to_le_bytes()); // bigWig magic
    bbi.extend_from_slice(&4u16.to_le_bytes()); // version
    bbi.extend_from_slice(&2u16.to_le_bytes()); // zoom levels
    bbi.extend_from_slice(&312u64.to_le_bytes()); // chr tree offset
    bbi.extend_from_slice(&200u64.to_le_bytes()); // full data offset
    bbi.extend_from_slice(&280u64.to_le_bytes()); // full index offset
    bbi.extend_from_slice(&0u16.to_le_bytes()); // field count
    bbi.extend_from_slice(&0u16.to_le_bytes()); // defined field count
    bbi.extend_from_slice(&0u64.to_le_bytes()); // autoSql offset
    bbi.extend_from_slice(&112u64.to_le_bytes()); // total summary offset
    bbi.extend_from_slice(&32768u32.to_le_bytes()); // uncompress buffer size
    bbi.extend_from_slice(&[0u8; 8]); // reserved
    for level in 0..2u32 {
        bbi.extend_from_slice(&(10 * (level + 1)).to_le_bytes());
        bbi.extend_from_slice(&0u32.to_le_bytes());
        bbi.extend_from_slice(&400u64.to_le_bytes());
        bbi.extend_from_slice(&500u64.to_le_bytes());
    }
    bbi.resize(312, 0);
    // A one-node chromosome tree at 312.
    bbi.extend_from_slice(&0x78CA_8C91u32.to_le_bytes());
    bbi.extend_from_slice(&256u32.to_le_bytes()); // block size
    bbi.extend_from_slice(&4u32.to_le_bytes()); // key size
    bbi.extend_from_slice(&8u32.to_le_bytes()); // value size
    bbi.extend_from_slice(&1u64.to_le_bytes()); // item count
    bbi.extend_from_slice(&[0u8; 8]); // reserved
    bbi.extend_from_slice(&[1u8, 0]); // leaf, reserved
    bbi.extend_from_slice(&1u16.to_le_bytes()); // child count
    bbi.extend_from_slice(b"chr1");
    bbi.extend_from_slice(&0u32.to_le_bytes()); // chr id
    bbi.extend_from_slice(&1000u32.to_le_bytes()); // chr size
    out.push(("a bigwig header", bbi));

    // A wig section, fixedStep, with items after it.
    let mut wig = Vec::new();
    wig.extend_from_slice(&0u32.to_le_bytes()); // chr id
    wig.extend_from_slice(&0u32.to_le_bytes()); // chr start
    wig.extend_from_slice(&100u32.to_le_bytes()); // chr end
    wig.extend_from_slice(&10u32.to_le_bytes()); // item step
    wig.extend_from_slice(&10u32.to_le_bytes()); // item span
    wig.push(3); // fixedStep
    wig.push(0); // reserved
    wig.extend_from_slice(&10u16.to_le_bytes()); // item count
    for i in 0..10 {
        wig.extend_from_slice(&(i as f32).to_le_bytes());
    }
    out.push(("a wig section", wig));

    // Bed records: three coordinates and a NUL-terminated tail each.
    let mut bed = Vec::new();
    for i in 0..5u32 {
        bed.extend_from_slice(&0u32.to_le_bytes());
        bed.extend_from_slice(&(i * 10).to_le_bytes());
        bed.extend_from_slice(&(i * 10 + 5).to_le_bytes());
        bed.extend_from_slice(b"name\tscore");
        bed.push(0);
    }
    out.push(("bed records", bed));

    // A BAM header: magic, a text block, and one reference — inside a BGZF
    // block, since that is what `bam::header::read` reads through. Handed the
    // bytes raw it would only ever exercise the codec, and the header parser
    // this is meant to reach would never run.
    let mut bam = Vec::new();
    bam.extend_from_slice(b"BAM\x01");
    let text = b"@HD\tVN:1.6\n@SQ\tSN:chr1\tLN:1000\n";
    bam.extend_from_slice(&(text.len() as u32).to_le_bytes());
    bam.extend_from_slice(text);
    bam.extend_from_slice(&1u32.to_le_bytes()); // n_ref
    bam.extend_from_slice(&5u32.to_le_bytes()); // l_name
    bam.extend_from_slice(b"chr1\0");
    bam.extend_from_slice(&1000u32.to_le_bytes());
    out.push(("a bam header", bgzf_block(&bam)));

    // A BAI: magic, one reference, one bin with one chunk, one linear slot.
    let mut bai = Vec::new();
    bai.extend_from_slice(b"BAI\x01");
    bai.extend_from_slice(&1u32.to_le_bytes()); // n_ref
    bai.extend_from_slice(&1u32.to_le_bytes()); // n_bin
    bai.extend_from_slice(&4681u32.to_le_bytes()); // bin
    bai.extend_from_slice(&1u32.to_le_bytes()); // n_chunk
    bai.extend_from_slice(&0u64.to_le_bytes()); // chunk begin
    bai.extend_from_slice(&65536u64.to_le_bytes()); // chunk end
    bai.extend_from_slice(&1u32.to_le_bytes()); // n_intv
    bai.extend_from_slice(&0u64.to_le_bytes());
    out.push(("a bam index", bai));

    // A BAM record block: one alignment, hand-built.
    let mut record = Vec::new();
    let read_name = b"read1\0";
    // One CIGAR op: length in the high 28 bits, operation in the low 4.
    // `M` is 0, so this is `10M`.
    let cigar: [u32; 1] = [10 << 4];
    let seq = [0x12u8, 0x48]; // four bases
    let qual = [30u8; 4];
    // block_size counts everything after itself: the 32 fixed bytes from refID
    // to tlen, then the four variable-length fields.
    let body_len = 32 + read_name.len() + cigar.len() * 4 + seq.len() + qual.len();
    record.extend_from_slice(&(body_len as u32).to_le_bytes());
    record.extend_from_slice(&0i32.to_le_bytes()); // refID
    record.extend_from_slice(&100i32.to_le_bytes()); // pos
    record.push(read_name.len() as u8); // l_read_name
    record.push(60); // mapq
    record.extend_from_slice(&4681u16.to_le_bytes()); // bin
    record.extend_from_slice(&(cigar.len() as u16).to_le_bytes());
    record.extend_from_slice(&0u16.to_le_bytes()); // flag
    record.extend_from_slice(&4u32.to_le_bytes()); // l_seq
    record.extend_from_slice(&(-1i32).to_le_bytes()); // next refID
    record.extend_from_slice(&(-1i32).to_le_bytes()); // next pos
    record.extend_from_slice(&0i32.to_le_bytes()); // tlen
    record.extend_from_slice(read_name);
    for op in cigar {
        record.extend_from_slice(&op.to_le_bytes());
    }
    record.extend_from_slice(&seq);
    record.extend_from_slice(&qual);
    out.push(("a bam record", record));

    // A HiC header: magic, version, footer position, genome, attributes,
    // chromosomes and resolutions.
    let mut hic = Vec::new();
    hic.extend_from_slice(b"HIC\0");
    hic.extend_from_slice(&8i32.to_le_bytes()); // version
    hic.extend_from_slice(&200i64.to_le_bytes()); // footer position
    hic.extend_from_slice(b"mm10\0");
    hic.extend_from_slice(&1i32.to_le_bytes()); // one attribute
    hic.extend_from_slice(b"software\0made-up\0");
    hic.extend_from_slice(&2i32.to_le_bytes()); // two chromosomes
    hic.extend_from_slice(b"All\0");
    hic.extend_from_slice(&2000i32.to_le_bytes());
    hic.extend_from_slice(b"chr1\0");
    hic.extend_from_slice(&1000i32.to_le_bytes());
    hic.extend_from_slice(&1i32.to_le_bytes()); // one bp resolution
    hic.extend_from_slice(&5000i32.to_le_bytes());
    hic.extend_from_slice(&0i32.to_le_bytes()); // no frag resolutions
    hic.resize(200, 0);
    // A footer at 200: byte count, then one master index entry.
    hic.extend_from_slice(&64i32.to_le_bytes());
    hic.extend_from_slice(&1i32.to_le_bytes());
    hic.extend_from_slice(b"0_0\0");
    hic.extend_from_slice(&300i64.to_le_bytes());
    hic.extend_from_slice(&128i32.to_le_bytes());
    hic.extend_from_slice(&0i32.to_le_bytes()); // no expected value vectors
    hic.extend_from_slice(&0i32.to_le_bytes()); // none normalized either
    hic.extend_from_slice(&0i32.to_le_bytes()); // no normalization vectors
    out.push(("a hic header", hic));

    out
}

/// One BGZF block wrapping `payload`: a gzip member carrying the `BC` extra
/// field the format adds, which is what says how long the block is.
fn bgzf_block(payload: &[u8]) -> Vec<u8> {
    use std::io::Write as _;
    let mut encoder = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::new(6));
    encoder
        .write_all(payload)
        .expect("deflate to a Vec cannot fail");
    let deflated = encoder.finish().expect("deflate to a Vec cannot fail");

    let total = 18 + deflated.len() + 8;
    let mut out = Vec::with_capacity(total);
    out.extend_from_slice(&[0x1f, 0x8b, 8, 4, 0, 0, 0, 0, 0, 0xff]);
    out.extend_from_slice(&6u16.to_le_bytes()); // XLEN
    out.extend_from_slice(b"BC");
    out.extend_from_slice(&2u16.to_le_bytes()); // SLEN
    out.extend_from_slice(&((total - 1) as u16).to_le_bytes()); // BSIZE
    out.extend_from_slice(&deflated);
    let mut crc = flate2::Crc::new();
    crc.update(payload);
    out.extend_from_slice(&crc.sum().to_le_bytes());
    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
    out
}

/// A zlib stream that inflates to far more than it costs, which is what the
/// block decoder's cap is for.
///
/// `#[ignore]` because it builds a gibibyte and deflates it, which is sixteen
/// seconds against the rest of this file's half a second — and `bbi::block`
/// already proves the *rule* in microseconds, against an injected limit. What
/// this adds is that the real constant is the one wired in, which is worth
/// checking on demand (`cargo test -- --ignored`) rather than on every run.
#[test]
#[ignore = "allocates a gibibyte; run with --ignored"]
fn a_decompression_bomb_is_refused_rather_than_inflated() {
    use std::io::Write as _;
    let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
    // A gibibyte and a byte of zeros: about a megabyte deflated, and one byte
    // past what a block is allowed to inflate to.
    encoder.write_all(&vec![0u8; (1 << 30) + 1]).unwrap();
    let bomb = encoder.finish().unwrap();
    assert!(
        bomb.len() < 8 << 20,
        "the bomb should be small: {}",
        bomb.len()
    );

    let err = crate::bbi::block::decompress(Bytes::from(bomb), 4096, "bomb.bigwig")
        .unwrap_err()
        .to_string();
    assert!(err.contains("exceeds limit"), "{err}");
}

/// The one invariant a fuzz test cannot check by not crashing: that a parser
/// which *accepts* something still hands back what the bytes said.
///
/// Here so the harness above cannot pass by having every parser refuse
/// everything, which is a way of satisfying "never panics" that would be
/// useless.
#[test]
fn the_valid_seeds_parse_as_themselves() {
    let seeds: std::collections::HashMap<_, _> = valid_headers().into_iter().collect();

    let source = MemorySource::new(seeds["a bigwig header"].clone());
    let header = crate::bbi::header::read_header(&source).expect("the seed is a bbi header");
    assert_eq!(header.version, 4);
    assert_eq!(header.zoom_levels, 2);
    let (map, _) = crate::bbi::chr_tree::read(&source, 312).expect("and carries a tree");
    assert_eq!(map.names(), ["chr1"]);

    let wig = &seeds["a wig section"];
    let header = crate::bbi::block::read_wig_header(wig, "seed").expect("a wig section");
    assert_eq!(header.item_count, 10);
    let item = crate::bbi::block::read_wig_item(wig, &header, 3, "seed").expect("item 3");
    assert_eq!((item.start, item.end, item.value), (30, 40, 3.0));

    let mut seen = 0;
    crate::bbi::block::visit_bed_records(&seeds["bed records"], "seed", |_, _, _| seen += 1)
        .expect("bed records");
    assert_eq!(seen, 5);

    let source = MemorySource::new(seeds["a bam header"].clone());
    let (_, map) = crate::bam::header::read(&source).expect("a bam header");
    assert_eq!(map.names(), ["chr1"]);

    let source = MemorySource::new(seeds["a bam index"].clone());
    crate::bam::bai::BamIndex::read(&source).expect("a bam index");

    let names = std::sync::Arc::new(vec!["chr1".to_string()]);
    let records = crate::bam::record::decode_block(
        &Bytes::from(seeds["a bam record"].clone()),
        true,
        &crate::bam::EntryFilter {
            chr_index: None,
            start: 0,
            end: None,
            standard_flags: false,
        },
        &names,
        "seed",
    )
    .expect("a bam record");
    assert_eq!(records.len(), 1);
    assert_eq!(records[0].start(), 100);

    let source = MemorySource::new(seeds["a hic header"].clone());
    let header = crate::hic::header::read_header(&source).expect("a hic header");
    assert_eq!(header.version, 8);
    assert_eq!(header.genome_id, "mm10");
    let footer = crate::hic::header::read_footer(&source, &header).expect("and a footer");
    assert_eq!(footer.master_index.len(), 1);
}