1
0
mirror of https://github.com/lz4/lz4.git synced 2026-10-24 01:48:57 +03:00
Commit Graph

4080 Commits

Author SHA1 Message Date
Yann Collet
317389aaff cli: share file name building of multiple-files modes
Frame and legacy compression of multiple files each built the name of
the compressed file, with the same buffer management : initial
allocation, growth, strcpy + strcat of the suffix, and stdout special
case. They now share LZ4IO_compressedFileName(), and decompression of
multiple files shares its buffer management, LZ4IO_reserveFileName().
2026-10-21 12:44:20 -07:00
Yann Collet
9e8bc239b8 Merge pull request #1918 from lz4/cli_legacy_rm
cli: legacy format honors --rm and keeps file attributes
2026-10-21 10:54:36 -07:00
Yann Collet
f11eeb613c cli: legacy format honors --rm and keeps file attributes
Single and multi-threaded frame compression duplicated the opening of
files, the preparation of preferences, and the end of file processing :
closing files, copying owner, permissions and modification time, --rm,
final status. Legacy compression had none of this end : `lz4 -l` ignored
`--rm`, and the compressed file didn't keep the source's attributes.

The end of file processing is now LZ4IO_finishCompressedFile(), shared by
frame and legacy compression, and LZ4IO_compressFilename_extRess() opens
files and prepares preferences for both frame compressors, which only
compress.

Also, LZ4IO_compressFilename_extRess() now reports a processed size of 0
when a file can't be opened, instead of leaving it uninitialized : with
-m, a missing file added a stale or garbage size to the final speed
summary (found by scan-build).
2026-10-21 10:30:59 -07:00
Yann Collet
dcd73e8e4d Merge pull request #1915 from akashdeshmukh-git/cli-reject-malformed-long-args
cli: reject malformed --fast= and --threads arguments
2026-10-20 15:49:17 -07:00
Yann Collet
911d1ae36b Merge pull request #1914 from akashdeshmukh-git/fix-truncated-skippable-frame
lz4io: detect truncated skippable frames in regular files
2026-10-20 11:39:44 -07:00
Yann Collet
2e12728a18 Merge pull request #1912 from lz4/wildcopy64_override
lz4: LZ4_WILDCOPY64 can be set from the build command
2026-10-20 09:55:21 -07:00
Yann Collet
5542d86ef3 Merge branch 'dev' into wildcopy64_override 2026-10-20 08:26:08 -07:00
Yann Collet
1deef0b70c Merge pull request #1913 from lz4/xxh_bad_function_cast
xxhash: no -Wbad-function-cast warning
2026-10-20 08:21:58 -07:00
akashdeshmukh-git
cebeba6e3e cli: reject malformed --fast= and --threads arguments
`--fast=5x` was accepted as `--fast=5`: characters following the
number were silently ignored, unlike `--threads=5x` which is rejected.

`--threadsX 2` was accepted as `--threads 2`, since the long command
was matched as a prefix only.

Both are now rejected with the usage message.
2026-10-20 08:32:54 +00:00
akashdeshmukh-git
26f121584a lz4io: detect truncated skippable frames in regular files
fseek() succeeds beyond end of file, so a skippable frame whose
declared size exceeds the remaining input was silently accepted
when reading from a file (`lz4 -t`, `lz4 -d` and `lz4 --list` all
reported success), while the same input from a pipe correctly failed
with "Error 43 : cannot skip skippable area".

fseek_u32() now seeks to the last byte of the area and reads it,
so truncation is detected the same way for files and pipes.
2026-10-20 08:32:54 +00:00
Yann Collet
060275ab9f Merge pull request #1911 from lz4/lz4f_decompress_gather
lz4frame: LZ4F_decompress() refactor
2026-10-20 00:01:04 -07:00
Yann Collet
8b8c415d79 xxhash: no -Wbad-function-cast warning
XXH_isLittleEndian() returned an int,
and each caller cast this function call to XXH_endianness,
which triggers -Wbad-function-cast (6 warnings, gcc and clang).

It now returns an XXH_endianness.
The casts at call sites are kept:
they still support an XXH_CPU_LITTLE_ENDIAN defined externally as an int,
including in C++ builds, where int to enum requires a cast.
Generated code is unchanged (-O3).

-Wbad-function-cast is added to the lib/Makefile warning flags,
so that a regression is caught by existing builds.

Reported, with a first fix, by @brainy in #1711.
2026-10-19 23:50:05 -07:00
Yann Collet
af4b7386f2 lz4: LZ4_WILDCOPY64 can be set from the build command
Like LZ4_FAST_DEC_LOOP, LZ4_WILDCOPY64 can now be enabled or disabled
at compile time (-DLZ4_WILDCOPY64=0 or 1). Defaults are unchanged.

#1804 proposed enabling both on wasm, measuring +21% (default) and
+25% (HC9) decompression speed on Apple M5 (Chromium 153, wasm32).
But a wasm module runs on any host, and on x86-64 the same change slows
decompression down (AMD Zen 4, Node 24 / V8 13.6, wasm32, Silesia) :
-5.8% / -7.8% on #1804's baseline, -1.5% / -3% on current dev,
with text files losing 10-20% and poorly compressible ones gaining 20-40%.
So it's not enabled by default : a wasm build can opt in with
-DLZ4_FAST_DEC_LOOP=1 -DLZ4_WILDCOPY64=1, which produces the same module
as #1804 applied on dev.

Based on #1804 by @JCash.
2026-10-19 23:31:08 -07:00
Yann Collet
3e337913ce Merge pull request #1910 from lz4/compress_end_limits
lz4: set the compressor's end limits after the input size check
2026-10-19 23:26:13 -07:00
Yann Collet
79d09aa0d9 lz4frame: LZ4F_decompress(): gather input with LZ4F_gatherInput()
Block header, compressed block, block checksum, frame checksum and
skippable frame size each gathered their input bytes their own way,
4 of them with a "get" stage reading directly from src and a "store"
stage buffering it, joined by case labels nested within if statements.
LZ4F_gatherInput() now provides these bytes, directly from src when
possible, otherwise buffered over as many calls as needed: each pair
becomes a single stage, 11 stages instead of 15.

The frame header keeps its 2 stages: LZ4F_getFrameInfo() relies on
dstage_storeFrameHeader, and LZ4F_decodeHeader() may request more bytes.

No behavior change.
2026-10-19 23:02:10 -07:00
Yann Collet
aecc724040 lz4: set the compressor's end limits after the input size check
`mflimitPlusOne` and `matchlimit` pointed before the input buffer
for inputs shorter than `LZ4_minLength`, which is undefined behavior.
They are only used once the input is long enough.
2026-10-19 22:49:18 -07:00
Yann Collet
29f736debc Merge pull request #1909 from lz4/lz4f_decompress_dedup
lz4frame: LZ4F_decompress(): factor block decoding and history move
2026-10-19 22:46:25 -07:00
Yann Collet
f20d44534e Merge pull request #1908 from lz4/cxx_modules_headers
headers: include dependencies outside of extern "C"
2026-10-19 22:46:08 -07:00
Yann Collet
aca5592874 lz4frame: LZ4F_decompress(): factor block decoding and history move
The 2 compressed block decoding paths (directly into dst, or into tmpOut)
duplicated the dictionary size clamp, LZ4_decompress_safe_usingDict(), its
error check, and the content checksum and remaining size updates: they now
share LZ4F_decodeBlock(). The history move in front of tmpOut, written both
in LZ4F_updateDict() and at the end of LZ4F_decompress(), becomes
LZ4F_moveDictBeforeTmpOut().

Also fix a wrong comment and a stray unary plus.

No behavior change.
2026-10-19 21:46:21 -07:00
Yann Collet
6f6e186b3a headers: include dependencies outside of extern "C"
C++ forbids including a header within a linkage specification.
With clang C++ modules (-fmodules), including lz4.h or lz4hc.h failed :
<stdint.h> was imported within extern "C".

All public headers (lz4.h, lz4hc.h, lz4frame.h, lz4file.h) now include
their dependencies before opening extern "C".

New test : make -C tests test-cxx-modules (requires clang),
run in CI with clang on Linux, and on macOS.

Fixes #1600. Based on #1815 by @sergioperezcheco.
2026-10-19 21:34:16 -07:00
Yann Collet
2de9a161a0 Merge pull request #1907 from lz4/lz4mid_simplify
lz4hc: LZ4MID_compress(): factor match evaluation
2026-10-19 21:22:59 -07:00
Yann Collet
e94b51bbd0 Merge pull request #1906 from lz4/bench_decode_only_capacity
bench: fix decode-only result buffer overflow
2026-10-19 21:22:34 -07:00
Yann Collet
f676142fff lz4hc: LZ4MID_compress(): factor match candidate evaluation
The 4 near-identical blocks evaluating a candidate (long or short match,
in prefix or extDict) are replaced by LZ4MID_countMatch(). Each search
stage now has a single "match found" exit: 3 goto instead of 5. The only
_lz4mid_dest_overflow use is inlined: 2 labels instead of 3.
Also fix 2 comments, and log found matches in one place.

No output change.
2026-10-19 20:58:23 -07:00
Yann Collet
c661d143b5 Merge pull request #1782 from neilpang/freebsd-ci
[ci] restore FreeBSD testing on GitHub Actions
2026-10-19 20:47:55 -07:00
Yann Collet
bf2b488182 bench: decode-only mode shares the whole result buffer
`lz4 -b -d` reserved 255x each frame's compressed size in the result
buffer, but capped that buffer at LZ4_MAX_INPUT_SIZE. With multiple
large files, later frames pointed past the buffer : 4 files of 4 MB
crash. The decode loop also recomputed each capacity with an INT_MAX
cap instead, so a frame decoding to more than LZ4_MAX_INPUT_SIZE wrote
out of bounds.

Decode-only output is not checked, so all frames now decode at the
start of the result buffer, each with its whole capacity. The capacity
is computed once, when the block table is laid out, and reused by the
decode loop.

Bug since v1.9.4. Based on #1770 by @arshsmith1
2026-10-19 20:46:34 -07:00
Yann Collet
4eba01144b Merge pull request #1905 from lz4/legacy_zero_endmark
cli: accept the Linux kernel's legacy end marker
2026-10-19 20:33:11 -07:00
Yann Collet
73db319b24 Merge pull request #1904 from lz4/lz4mid_hash_positions
lz4hc: LZ4MID_compress() optimization
2026-10-19 20:28:56 -07:00
Yann Collet
a2e2466490 ci: align the FreeBSD workflow with the other workflows
- actions/checkout v7.0.1, as in the other workflows
- vmactions/freebsd-vm v1.5.9, which ignores `usesh` (scripts always
  run with sh), so the input is dropped
- list freebsd.yml in .github/workflows/README.md
2026-10-19 19:11:34 -07:00
Yann Collet
fe6efeb514 cli: legacy decoder counts decoded bytes in its writer thread
The multithreaded legacy decoder updated the decoded-byte counter
in its decoding thread, while the writer thread read it to display progress :
a data race, reported by ThreadSanitizer on any multi-block legacy stream.
The writer thread is now the only one to update and read it.
The final count is read after both threads complete.

Found by @agamal026 in #1890
2026-10-19 19:05:22 -07:00
Yann Collet
ac2eba0f1a cli: accept the Linux kernel's legacy end marker
The kernel's legacy decoder also stops at a zero Block Compressed Size,
and initramfs builders (mkinitcpio) append these four zero bytes
to delimit concatenated archives.
lz4 passed it to LZ4_decompress_safe() as an empty block,
and failed with "Error 64 : Decoding Failed".

A zero block size now ends the legacy frame,
in both legacy decoders and in --list,
and decoding continues with the next frame, if any.
It was never a valid block : a compressed block is at least 1 byte.

The format spec (v1.6.5) documents it as an extension :
encoders should not produce it, since older decoders reject it.

Fixes #1889, based on #1890 by @agamal026
2026-10-19 19:05:22 -07:00
neil
bc4551652d ci: run the FreeBSD tests on GitHub Actions 2026-10-19 18:57:35 -07:00
Yann Collet
a431b76060 Merge pull request #1903 from lz4/bench_small_blocks_overflow
bench: fix compressed buffer size with small blocks
2026-10-19 18:56:22 -07:00
Yann Collet
9584f8433a lz4hc: LZ4MID_compress(): fill hash tables at the actual match start
The beginning of a match was inserted at ipIndex, the index of the search
position. But ip moves before that point, when a longer match is found at
ip+1, or when the match is extended backward (catch back): these hash table
entries then pointed to the wrong position, and overwrote useful ones.

Level 2 compresses 0.1-0.5% better, and runs 0.8-1.9% fewer instructions.
2026-10-19 17:08:08 -07:00
Yann Collet
594ec184b8 Merge pull request #1902 from lz4/decoder_ring_margin
lz4: decoder supports rings using former LZ4_DECODER_RING_BUFFER_SIZE()
2026-10-19 16:48:08 -07:00
Yann Collet
ed8b913adc Merge pull request #1901 from lz4/hc_hashChain_simplify
lz4hc: LZ4HC_compress_hashChain(): simplify flow
2026-10-19 16:44:26 -07:00
Yann Collet
c2ecde85b9 bench: fix compressed buffer size with small blocks
BMK_benchMem() sized the compressed buffer with a margin of 1024 bytes
per block, computed as `maxNbBlocks * 1024` in 32-bit unsigned
arithmetic. With 4M blocks or more (e.g. `-B32` on a 128 MiB input),
the product wrapped around, and the buffer could be smaller than the
LZ4_compressBound() of all blocks, making the benchmark write past its end.

LZ4_compressBound() adds exactly 16 bytes per block, so the margin is
now `maxNbBlocks * 16`, computed in size_t.
2026-10-19 16:28:38 -07:00
Yann Collet
f4f7a9882c lz4: decoder still supports rings of the former LZ4_DECODER_RING_BUFFER_SIZE()
A ring buffer of LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize) is allocated statically,
so its size is compiled into applications.
16d695b1 enlarged its margin from 14 to 64 bytes to fit the fast loop's wide copies,
leaving applications compiled with the former macro exposed to corrupted output.

The decoder now also supports the former margin, without advertising it.
When decoding restarts at the beginning of the ring, the oldest referenced history
lies in the external dictionary, ahead of the write position, at a constant distance.
When this distance is shorter than FASTLOOP_SAFE_DISTANCE, the block is decoded by the safe loop,
which writes at most 16 bytes beyond a copy, like the v1.8.x decoder the 14-byte margin was sized for.
This only concerns rings with less than 64 bytes of margin, hence never the current macro,
for the blocks following such a restart, until 64 KB have been decoded.
Other decoders are unaffected : the test is resolved at compile time outside of usingExtDict.

Applications compiled with older headers decode correctly with the new library, without recompiling.

The static assert now checks what correctness requires, i.e. the safe loop's margin,
which the former macro satisfies : on this point, lz4.c compiles again with an older lz4.h (#1863).

The fuzzer's worst-case ring test now also runs with the former ring size :
it fails without this change, and passes with it.
The other ring condition (decoder ring larger than the encoder's by maxBlockSize + 64 bytes, 2f449bb3)
is unchanged : the decoder can't know the encoder's ring size.
2026-10-19 16:24:45 -07:00
Yann Collet
875d99dd82 lz4hc: LZ4HC_compress_hashChain(): simplify the m1/m2/m3 flow
With 3 ascending matches, m2 was already shifted against m1 at _Search3,
so the second LZ4HC_splitOverlap() always reduced to cutting m1 where m2
starts. Remove it, and share this path with the "no better m3" one,
which encodes m1 the same way: m2 then becomes m1, every sequence is
encoded as m1, and _dest_overflow no longer needs m1 = m2.
_Search3 becomes a loop: 3 labels instead of 4, 8 goto instead of 11.

Also add state comments, and fix 2 wrong ones: start0 != ip is common
after the restore test, and when m3 starts after m1, m2 is not removed,
it becomes the fallback m0.

No output change.
2026-10-19 16:12:49 -07:00
Yann Collet
0aea8e59bb Merge pull request #1899 from lz4/decompress_shared_offset_copy
lz4: LZ4_decompress_generic(): safe loop refactor
2026-10-19 15:22:07 -07:00
Yann Collet
2330c5ddc1 lz4: LZ4_decompress_generic(): safe loop copies matches with LZ4_memcpy_using_offset_base()
The safe loop had its own copy of LZ4_memcpy_using_offset_base(), the
fast loop's helper: the same inc32table/dec64table trick for offsets
below 8, followed by the same 8-byte copies. Use the shared helper.

It now supports offset 1 (first 4 bytes copied one by one), is defined
without LZ4_FAST_DEC_LOOP, and follows the safe loop's structure: the
second 8 bytes are copied directly, LZ4_wildCopy8() only beyond 16 bytes.

No behavior change. Instruction counts move by less than 0.15%.
Speed: +0% to +3.7% on all decoders without LZ4_FAST_DEC_LOOP;
with it, -3% to +4% depending on decoder and code alignment.
2026-10-19 13:29:48 -07:00
Yann Collet
558a11b82b Merge pull request #1900 from lz4/cli_mozlz4
cli: decode Firefox's mozlz4 files
2026-10-19 12:54:03 -07:00
Yann Collet
7d3390ea39 Merge pull request #1898 from lz4/wildCopy64_all_targets
LZ4_wildCopy64 on x86-64
2026-10-19 12:43:30 -07:00
Yann Collet
c33d5dc701 lz4: LZ4_decompress_generic(): LZ4_wildCopy64 on x86-64
LZ4_wildCopy64 (long literals, and matches with offset >= 64) was restricted to aarch64.
It speeds up decompression on x86-64 too.
AMD EPYC Genoa (Zen 4), gcc 11, silesia files :
level 1 : +5.1% (geomean), up to +18.8% (sao) ; level 9 : +2.6%, up to +9.8% (xml).
Worst cases : -1.7% (sao, level 9), -1.0% (reymont, level 9).

Not enabled on 32-bit x86 (same machine, gcc 12 -m32) :
without SSE2 : -0.8% / -1.9% (geomean, levels 1 / 9), down to -10.2% (osdb) ;
with SSE2 : +0.7% / +1.0%, from -3.0% (sao) to +12.1% (x-ray).
Other targets are unchanged.

Proposed for x86 in #1407 by Nicolas De Carli.
2026-10-19 11:07:00 -07:00
Yann Collet
70860f1790 cli: decode Firefox's mozlz4 files
Firefox stores its .jsonlz4 / .mozlz4 files (session, bookmark backups,
search engines) as "mozLz40\0", the decompressed size (LE32),
then a single LZ4 block extending to the end of the file
(see MozLZ4 in Firefox's xpcom/ioutils/IOUtils.cpp).

`lz4 -d`, `lz4 -t` and `lz4cat` now recognize this format,
and decode it like Firefox's reader :
- the decompressed size is only used as decoding capacity
- empty content has no block (Firefox doesn't write one)

Allocations are bounded : sizes > LZ4_MAX_INPUT_SIZE are rejected,
as they can't be compressed into a single block,
and input is read up to LZ4_COMPRESSBOUND(size), the largest valid block.

Data starting with the first half of the magic only ("mozL") keeps
the previous behavior : `lz4cat` passes it through unchanged,
including the bytes read to check the second half.

Decoding only : lz4 doesn't produce this non-standard format.

Based on #1259 by @UnitedMarsupials, rewritten for current dev
(no mmap, bounded allocations, full magic check, tests).
2026-10-19 10:49:30 -07:00
Yann Collet
a48e9790a2 Merge pull request #1897 from lz4/decompress_align_loops
lz4: LZ4_decompress_generic(): fast loop refactor
2026-10-19 09:16:31 -07:00
Yann Collet
b5be6dc6bf lz4: LZ4_decompress_generic(): fast loop decodes the match length like the safe loop
The fast loop checked for the safe loop handover twice, once per branch
of the match length token, and nested its 18-byte copy shortcut in the
short match branch. Decode the full match length first, then check the
handover once, then try the shortcut, in the same order as the safe loop.

No behavior change. Without LZ4_FAST_DEC_LOOP the generated code is
identical; with it, instruction counts move by less than 0.2%.
2026-10-19 08:14:44 -07:00
Yann Collet
4b523c7ed6 Merge pull request #1642 from Benjamin-Philip/bp/docs-clean-up-stream-example
Touch up line by line compression example
2026-10-19 08:01:25 -07:00
Yann Collet
75c0dd504c Merge pull request #1766 from lz4/dependabot/github_actions/github/codeql-action/upload-sarif-4.36.2
build(deps): bump github/codeql-action/upload-sarif from 4.35.1 to 4.39.0
2026-10-19 08:00:44 -07:00
Yann Collet
1b56026ae3 Merge pull request #1767 from lz4/dependabot/github_actions/actions/setup-python-6.3.0
build(deps): bump actions/setup-python from 6.2.0 to 7.0.0
2026-10-19 08:00:16 -07:00
Yann Collet
c679b6813f Merge pull request #1896 from lz4/ci_scorecard_v2.5.0
ci: bump ossf/scorecard-action to v2.5.0
2026-10-19 07:59:42 -07:00