@@ -16,6 +16,8 @@ The project provides:
1616 stream suitable for parsers such as [ paraseq] ;
1717- opt-in random-access index construction, interoperable index formats, and a
1818 decoded-output ` Read + Seek ` adapter;
19+ - opt-in newline counting, line-annotated indexes, and indexed seeking by
20+ zero-based line number;
1921- decoding of non-seekable compressed input such as standard input, a FIFO, a
2022 process substitution, or a socket.
2123
@@ -321,7 +323,51 @@ Format parsers apply explicit checkpoint and window-allocation limits through
321323` IndexReadOptions ` . The native format represents every container. ` .gzi `
322324export requires an index proven to come from BGZF; GZIDX and gztool export
323325require gzip-family provenance; gztool line-aware export requires real line
324- counters and never invents them.
326+ counters and never invents them. The CLI detects formats from a bounded prefix,
327+ streams index parsing, rejects trailing bytes, and writes exports through a
328+ same-directory temporary file so failed conversions cannot truncate an
329+ existing index.
330+
331+ Line metadata is collected only when requested. Enabling
332+ [ ` DecoderBuilder::count_lines ` ] adds ` DecodeReport::line_count ` ; combining it
333+ with an explicit indexing operation also annotates every retained checkpoint:
334+
335+ ``` rust,no_run
336+ use rapidgzip_core::{Decoder, IndexOptions, IndexedReader};
337+ use std::fs::File;
338+ use std::io;
339+
340+ fn main() -> Result<(), Box<dyn std::error::Error>> {
341+ let decoder = Decoder::builder()
342+ .decoder_threads(8)
343+ .count_lines(true)
344+ .build()?;
345+ let source = File::open("reads.fastq.gz")?;
346+ let indexed = decoder.decode_with_index(
347+ &source,
348+ &mut io::sink(),
349+ IndexOptions::default(),
350+ )?;
351+ assert_eq!(indexed.decode.line_count, indexed.index.total_line_count());
352+
353+ let mut reader = IndexedReader::new(source, indexed.index)?;
354+ let byte_offset = reader.seek_to_line(1_000_000)?;
355+ println!("line 1000000 begins at decoded byte {byte_offset}");
356+ Ok(())
357+ }
358+ ```
359+
360+ A line offset is the number of ` b'\n' ` bytes preceding a position. Line zero
361+ begins at decoded byte zero. A final unterminated line does not increase
362+ ` line_count ` , while ` seek_to_line ` can still reach it after scanning from the
363+ nearest checkpoint. Counting happens once on final ordered bytes, after marker
364+ resolution, and is disabled by default. ` DecodeReport ` remains ` Copy ` because
365+ the result is an optional scalar. A line-aware index is published only when
366+ every retained checkpoint received an exact count; partial metadata is never
367+ presented as complete. Strict full-stream decoding with line counting enabled
368+ also recomputes imported per-checkpoint and total line counts, rejecting
369+ structurally valid but incorrect navigation metadata. Without line counting,
370+ imported line offsets remain explicitly trusted navigation data.
325371
326372` IndexedReader ` validates the index and any recorded source size before use.
327373Resuming at a gzip member or zlib-header checkpoint verifies that complete
@@ -346,6 +392,29 @@ rapidgzip-rust -P 16 reads.fastq.gz > reads.fastq
346392# Verify every member and discard decoded output.
347393rapidgzip-rust -P 16 --test reads.fastq.gz
348394
395+ # Count decoded bytes or newline bytes.
396+ rapidgzip-rust --count reads.fastq.gz
397+ rapidgzip-rust --count-lines reads.fastq.gz
398+
399+ # Decode and count in one pass. Counts use stderr when payload uses stdout.
400+ rapidgzip-rust -c --count --count-lines reads.fastq.gz > reads.fastq
401+
402+ # Build a native index, then use it for strict full-stream parallel decoding.
403+ rapidgzip-rust --test --export-index reads.rgzidx \
404+ --index-format native reads.fastq.gz
405+ rapidgzip-rust --import-index reads.rgzidx -c reads.fastq.gz > reads.fastq
406+
407+ # Extract byte and zero-based line ranges in the requested order.
408+ rapidgzip-rust --ranges '1KiB@4MiB,10L@1000L' -c reads.fastq.gz
409+
410+ # Authenticate the complete source before an imported random-access read.
411+ rapidgzip-rust --import-index reads.rgzidx --verify \
412+ --ranges '10L@1000L' -c reads.fastq.gz
413+
414+ # Export gztool version 1 with real line counters.
415+ rapidgzip-rust --count-lines --export-index reads.gzi \
416+ --index-format gztool-with-lines reads.fastq.gz
417+
349418# Refuse to overwrite an existing output file.
350419rapidgzip-rust -P 16 --output reads.fastq reads.fastq.gz
351420
@@ -356,12 +425,36 @@ cat reads.fastq.gz | rapidgzip-rust - > reads.fastq
356425rapidgzip-rust <(some_producer) > reads.fastq
357426```
358427
359- ` -P ` /` --threads ` is a maximum decoder-worker budget. Parallel paths bootstrap
428+ The CLI auto-detects gzip or zlib by default; raw DEFLATE requires
429+ ` --format raw-deflate ` . ` --chunk-size ` controls the decoded handoff size in
430+ KiB. Output defaults to stdout when redirected; at a terminal, a regular input
431+ derives a safe output filename using case-insensitive compression suffixes.
432+ Existing files require ` --force ` . Index export defaults to the native,
433+ format-neutral representation; gzip-specific interoperable formats must be
434+ selected explicitly.
435+
436+ ` -P ` /` --decoder-parallelism ` (` --threads ` is an alias) is a maximum
437+ decoder-worker budget. Parallel paths bootstrap
360438from the smaller of the affinity-visible processors and this requested budget,
361439then create more workers only while measurements justify them. They may retain
362440fewer active workers when the input exposes less parallel work, the consumer is
363441backpressured, or additional concurrency reduces throughput.
364442
443+ Imported indexes are never advisory. Full-stream operations use
444+ ` decode_from_index ` , and malformed, incomplete, or source-mismatched indexes
445+ fail without falling back. Range extraction uses ` IndexedReader ` ; a line range
446+ requires complete line metadata in an imported index or builds a line-aware
447+ index first. A seek from an interior DEFLATE checkpoint cannot authenticate
448+ the skipped prefix. For that reason, ` --verify ` on an imported range performs
449+ a complete strict indexed decode before extraction. Decoder options that would
450+ otherwise be ignored by an unverified imported range are rejected and explain
451+ that ` --verify ` is required. ` --no-verify ` , ` --sparse-windows ` , and the ` sequential ` and
452+ ` locked-read ` I/O methods are rejected because the current implementation
453+ cannot honor their semantics. Outside imported ranges, complete decode paths
454+ already verify their selected framing. ` --no-sparse-windows ` , ` -d ` , and ` -k `
455+ remain compatibility aliases. This is a deliberately compatible subset, not a
456+ claim that every rapidgzip CLI option is implemented.
457+
365458## Correctness and resource behavior
366459
367460- Every accepted gzip member is terminated by an actual final DEFLATE block
@@ -439,7 +532,8 @@ The integration suite covers gzip, zlib, raw DEFLATE, multi-member streams,
439532BGZF, corruption, format detection across short/interrupted reads, false header
440533candidates, output limits and exact sizes, index construction, indexed
441534full-stream parallel decode, seeking, cancellation, one-byte consumer buffers,
442- and direct paraseq consumption. Generated benchmark corpora and large
535+ line counting and seeking, CLI index/range workflows, and direct paraseq
536+ consumption. Generated benchmark corpora and large
443537sequencing files are deliberately not stored in the repository.
444538
445539## Releasing
@@ -481,6 +575,7 @@ MIT. See [LICENSE-BSD-3-CLAUSE] and [LICENSE-MIT].
481575[ `DecoderBuilder::input_page_size` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderBuilder.html#method.input_page_size
482576[ `DecoderBuilder::output_limit` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderBuilder.html#method.output_limit
483577[ `DecoderBuilder::expected_uncompressed_size` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderBuilder.html#method.expected_uncompressed_size
578+ [ `DecoderBuilder::count_lines` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderBuilder.html#method.count_lines
484579[ `DecoderReader` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderReader.html
485580[ `DecoderStats::path` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderStats.html#structfield.path
486581[ `DecoderStats::configured_workers` ] : https://docs.rs/rapidgzip-core/latest/rapidgzip_core/struct.DecoderStats.html#structfield.configured_workers
0 commit comments