Skip to content

Simplify crai reading and zlib_mem_inflate() - #2085

Merged
jkbonfield merged 3 commits into
samtools:developfrom
daviesrob:simpler-zlib-mem-inflate
Sep 9, 2026
Merged

Simplify crai reading and zlib_mem_inflate()#2085
jkbonfield merged 3 commits into
samtools:developfrom
daviesrob:simpler-zlib-mem-inflate

Conversation

@daviesrob

Copy link
Copy Markdown
Member

Use BGZF interface in place of hFILE in cram_index_load() so it no longer has to include its own decompression code. This allows zlib_mem_inflate() to be simplified as the only remaining caller both passes in the uncompressed size and rejects data that does not decompress to the size given, so logic for growing the output buffer can be removed.

Fixes a bug reported in #2081 where zlib_mem_inflate() could get stuck if the uncompressed size passed to it was 1, but the decompressed data needed more space.

Fixes a bug in cram_index_load() where the hFILE structure could be leaked if an error was detected when reading the index file.

Adds a quick test to cram_uncompress_block() so that GZIP blocks claiming an impossible compression ratio can be rejected without trying to decompress them first.

Closes #2081

Use BGZF via bgzf_open() etc. to simplify reading compressed
.crai index files.  As bgzf_open() calls hopen() itself, all
plugins etc. will still be supported, along with both plain-text
and gzip-compressed files.  As a side effect, BGZF-compressed
index files will now work (previously only plain-gzip was
supported).

Also adds bgzf_close() to the fail: block to fix a leak that
happened if an error was detected while reading the index file.

Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
As the only caller passes in the uncompressed size, and checks
the actual amount of data returned against that value, there
is no need to support growing the size of the output buffer.
This means decompression can be done in one shot, both when
using libdeflate and (as a cram block can't hold more than
INT_MAX bytes) zlib.

zlib_mem_inflate() can also be made static as the only caller
left is in cram/cram_io.c

Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
The maximum compression ratio of gzip is 1032 (LZ match of 258
bytes encoded in a 2bit huffman code) so it's not worth trying
to uncompress one that claims to do better than this.

Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
Comment thread cram/cram_io.c
goto fail;
}
data = new_data;
assert (*size > 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size comes from b->comp_size which is read from disk. We shouldn't be using assert of user-data derived fields.

We could just return NULL instead and the standard error handling will pick it up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently *size can never be zero due to this check. This assert is in place as a guard against future changes that may break that invariant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see that now, although if there's a check elsewhere then the assert seems pointless. As you say it's to guard against something later breaking, but a check and return NULL is an equally valid guard. I just feel uneasy about using assert for data directly decoded from a file, even if it's currently not possible to trigger.

Comment thread cram/cram_io.c

/* Starting point at uncompressed size, and scale after that */
data = malloc(data_alloc = csize*1.2+100);
assert(*size > 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, make this a return NULL instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly this can't happen at the moment. It exists to prevent future accidents.

Comment thread cram/cram_io.c
Comment on lines +912 to +915
// These should always be true due to type of cram_block::comp_size
// and cram_block::uncomp_size
assert(*size < UINT_MAX);
assert(csize < UINT_MAX);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it's broken data, size and csize could be negative. (I've no idea why the original CRAM format permitted this, but I'm assuming because it was developed in Java initially which has no unsigned types.)

Size is already checked above, but csize < 0 should be checked somewhere and return NULL again.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cram_read_block() currently ensures csize >= 0, so this should again never trigger. Or is it possible to get a block in a way where this might not be true?

Note that this very old assert in cram_uncompress_block() does the same check for uncomp_size. Also csize and *size here are size_t so cannot be negative.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's probably sufficient. However if we're adding more checks to this function then it's probably sensible to add both upper and lower bounds ones so it's trivial to read and understand without having to do a deep dive into all code path that led us here.

That said, I failed to spot they're size_t and not ssize_t, so ignore me. :-)

Comment thread cram/cram_io.c
Comment on lines +926 to +929
s.avail_in = (uInt) csize;
s.total_in = 0;
s.next_out = data;
s.avail_out = data_alloc;
s.avail_out = (uInt) *size;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary casts. We should have already checked signed vs unsigned sizes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The casts are to suppress compiler warnings about narrowing conversions, should they be in use.

We should really be better at adding casts like this, as they act as a handy warning about places where overflow bugs might be lurking.

@jkbonfield
jkbonfield merged commit 575276c into samtools:develop Sep 9, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants