Repository navigation
Add a checksum: option to Zstd.compress and Zstd::StreamingCompress - #164
Open
reidmorrison wants to merge 1 commit into
Open
reidmorrison wants to merge 1 commit into
reidmorrison wants to merge 1 commit into
Conversation
The zstd frame format has an optional checksum of the content, the low 4 bytes of its XXH64 hash, which libzstd verifies when it decompresses. The zstd command line tool writes it by default, but Zstd.compress and Zstd::StreamingCompress had no way to set ZSTD_c_checksumFlag, so data compressed with this gem never carried one, and corruption that leaves the frame structurally valid went undetected. Accept checksum: true in set_compress_params, which both entry points share. It defaults to false, libzstd's default, so frames are unchanged unless it is set. Like level: and dict:, nil means the default, and a value other than true or false raises ArgumentError. It is set before dict:, whose CDict branch returns early, so it applies with either kind of dictionary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
checksum:keyword toZstd.compressandZstd::StreamingCompress.new. Withchecksum: true, each frame ends with the content checksum that the zstd format defines (the low 4 bytes of the XXH64 hash of the uncompressed data), which libzstd verifies when it decompresses. The default isfalse, so nothing changes unless it is set.Motivation
IOStreams is a Ruby library that streams files through compression, encryption and storage (local files, S3, SFTP) one block at a time. On MRI it writes
.zstfiles withZstd::StreamingCompress. Thezstdcommand line tool writes the content checksum by default, and zstd-jni, which IOStreams uses on JRuby, can enable it, but zstd-ruby has no way to setZSTD_c_checksumFlag. So the.zstfiles IOStreams writes on MRI carry no checksum, and corruption that leaves a frame structurally valid decompresses into wrong data without an error.Flipping one random bit in a frame 1,500 times, and decompressing each copy with
Zstd::StreamingDecompress:checksum: falsechecksum: trueChange
set_compress_paramsincommon.h, whichZstd.compressandZstd::StreamingCompress#initializeshare, acceptschecksum:next tolevel:anddict:, and setsZSTD_c_checksumFlag. It follows the existing keywords:nil, or leaving it out, means the default,false, which is also libzstd's default.true,falseornilraisesArgumentError, after freeing the context as thedict:check does.dict:, whoseZstd::CDictbranch returns early. The checksum is a frame parameter, which aCDictdoes not override, so it works with either kind of dictionary.Decompression needs no change: libzstd verifies the checksum whenever a frame has one, so
Zstd.decompress,Zstd::StreamingDecompressandZstd::StreamReaderraiseRestored data doesn't match checksumwhen it does not match. The streaming classes check it at the end of the frame, after returning that frame's data, which the README notes.Zstd::StreamWriteronly passeslevel:on toStreamingCompress(notdict:either), so I left it unchanged.Compatibility
Without
checksum: true, the output is byte for byte the same as before. With it, the frame is the same apart from the checksum flag in the frame header descriptor and the 4 checksum bytes at its end, which every zstd decoder supports. The checksum matches the one thezstdcommand line tool writes for the same content, andzstd -taccepts the frames.Overlap with #162
#162 changes
set_compress_paramsso that it no longer frees the context before raising, because its callers free it instead. This PR follows the convention onmain, and frees the context before raisingArgumentErrorfor an invalidchecksum:. Git mergescommon.hcleanly either way (only the specs conflict), so whichever of the two is merged second has to drop thatZSTD_freeCCtx(ctx): a build with both crashes onZstd.compress('x', checksum: 1). If #162 is merged first, I will rebase this PR onto it.Tests
spec/zstd-ruby_spec.rbandspec/zstd-ruby-streaming-compress_spec.rbcheck that:checksum: falseornil, no checksum is written and the output is unchanged;checksum: truesets the flag and adds 4 bytes, which for'abc'are the low 4 bytes of its XXH64 hash, as thezstdcommand line tool writes them;Stringdictionary, aZstd::CDict, and acrossStreamingCompress#flush;Zstd.decompressandZstd::StreamingDecompress;ArgumentError.bundle exec rakepasses with 100 examples, 0 failures (91 onmain), on Ruby 3.2, 3.4 and 4.0 on macOS (arm64), with no new compiler warnings.🤖 Generated with Claude Code