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

4043 Commits

Author SHA1 Message Date
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
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
Yann Collet
023d031682 Merge pull request #1895 from lz4/saveDict_negative
Save dict negative
2026-10-19 07:59:05 -07:00
Yann Collet
8c920a0bf1 lineByLine example: LZ4 forgets old lines, it doesn't clear them
The data stays in the ring buffer; only the dictionary is reduced to
Line#X+1. Also fix a missing space.
2026-10-18 12:33:12 -07:00
dependabot[bot]
534645b9cb build(deps): bump github/codeql-action/upload-sarif
Bumps [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) from 4.35.1 to 4.39.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](c10b8064de...6f2ac81f5c)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 19:30:23 +00:00
dependabot[bot]
51d3d3d079 build(deps): bump actions/setup-python from 6.2.0 to 7.0.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a309ff8b42...5fda3b95a4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 19:28:25 +00:00
Yann Collet
779b355680 Merge pull request #1892 from arshsmith1/reject-negative-dictsize
lz4: reject negative dictSize in dictionary entry points
2026-10-18 12:22:34 -07:00
Yann Collet
47bed30b95 ci: bump ossf/scorecard-action to v2.5.0
Scorecard fails on every push: v2.4.0 pulls its image from gcr.io,
which has been retired ("denied: This API method requires billing").
The Scorecard v5.6.0 release notes ask action users to upgrade
to v2.5.0 or later, which pull from ghcr.io.
Supersedes #1659 (v2.4.3).
2026-10-18 12:03:37 -07:00
Yann Collet
354572d2d1 LZ4_saveDict(): a negative dictSize saves nothing
Follow-up to the previous commit: instead of returning early,
clamp a negative dictSize to 0, like LZ4_saveDictHC().
Returning early left the stream referencing the previous dictionary,
which the caller is about to overwrite, since it asked to save it:
the next block could then reference stale data.
With dictSize = 0, the stream continues without dictionary.

fuzzer: the next block copies the end of the dictionary,
and must decode without it (fails with the early return),
and saveBuffer is small enough for ASan to detect
the 64 KB copy (fails on dev).
2026-10-18 11:51:15 -07:00
arthuz-th
6b1d673e3c Reject negative dictionary sizes in LZ4_saveDict 2026-10-18 11:47:13 -07:00
Yann Collet
2e61b163a4 Merge pull request #1894 from lz4/cli_numeric_overflow
Cli numeric overflow
2026-10-18 11:43:06 -07:00
Yann Collet
07cdad5fca Merge pull request #1893 from lz4/decompress_last_literals
lz4: LZ4_decompress_generic(): simplify the last literals copy
2026-10-18 11:27:42 -07:00
Yann Collet
ccfc64e7b9 legacy format: same level to acceleration mapping as the frame format
The legacy compressor used acceleration = -cLevel,
while lz4frame and bench use -cLevel + 1.
Legacy levels were shifted by one: `lz4 -l --fast=1` produced the same
output as `lz4 -l -1`, and `-l --fast=2` matched the frame `--fast=1`.

Use the same mapping, capped at LZ4_ACCELERATION_MAX
(avoids a signed overflow with --fast=2147483647).
Levels >= 0 are unchanged.

test-lz4-basic.sh: `-l --fast=1` must compress less than `-l -1`
(fails on dev), and `-l --fast=2147483647` runs.
2026-10-18 11:20:02 -07:00
Yann Collet
97c68c2cd9 cap acceleration at LZ4_ACCELERATION_MAX, now defined in lz4.h
Follow-up to "lz4frame: avoid signed overflow", per the #1824 review:
saturate at LZ4_ACCELERATION_MAX rather than INT_MAX,
since higher values make no difference.

LZ4_ACCELERATION_DEFAULT and LZ4_ACCELERATION_MAX move from lz4.c
to the LZ4_STATIC_LINKING_ONLY section of lz4.h, which lz4.c, lz4frame.c,
bench.c and frametest.c already include, so their values derive from it.
The LZ4_compress_fast() documentation already referred to both constants.

bench.c computed the acceleration the same way (-cLevel + 1),
so `lz4 -b --fast=2147483647` hit the same signed overflow:
apply the same cap there.

test-lz4-basic.sh: --fast=2147483647 must produce the same frame
as --fast=65536 (fails on dev, where acceleration wraps to the default),
and its benchmark runs (caught by the UBSan CI job before this fix).

lz4.1: --fast values above 65536 make no difference.
2026-10-18 11:20:02 -07:00
Yann Collet
38b3ed8a33 cli: -b --fast=# benchmarks a single level
cLevelLast defaulted to -10000, meaning "unset".
With --fast=# and # > 10000, cLevel is below this sentinel,
so -b benchmarked all levels from -# up to -10000:
--fast=10003 ran 4 benchmarks, --fast=2147483647 ~2 billions.
Use INT_MIN as the sentinel instead.
2026-10-18 11:19:49 -07:00
Akash Deshmukh
e77252c9db lz4frame: avoid signed overflow for very negative compression levels
LZ4F_compressBlock() and LZ4F_compressBlock_continue() compute the
acceleration as -level + 1. For compressionLevel == INT_MIN + 1 this
overflows (INT_MAX + 1), and for INT_MIN the negation itself overflows.
Both are undefined behavior (UBSan reports them, e.g. with
`lz4 --fast=2147483647`). In practice the result wraps to a negative
acceleration, which LZ4_compress_fast*() replaces with the default (1),
so the fastest requested levels silently compress at default speed.

Compute the acceleration in a helper that saturates instead of
overflowing. LZ4_compress_fast*() already caps acceleration at
LZ4_ACCELERATION_MAX, so the result is unchanged for every other level.

Add a frametest check that levels -65537, INT_MIN + 1 and INT_MIN
produce the same frame as level -65536.
2026-10-18 11:19:49 -07:00
Yann Collet
e73c841a5c NEWS: reject overflowing numeric arguments in the CLI 2026-10-18 11:19:49 -07:00
Akash Deshmukh
3796322233 cli: reject numeric arguments that overflow
readU32FromChar() silently wrapped around on values larger than 32 bits,
and compression levels were then cast to int without a range check.
As a result, out-of-range arguments did something unrelated instead of
failing, for example:

  lz4 --fast=3000000000   compressed at maximum (HC) level
  lz4 -3000000000         used the fastest level (output larger than input)
  lz4 -4294967305         used level 9
  lz4 -B4294967300        used 64 KB blocks
  lz4 -B4194305K          used 1 KB blocks

readU32FromChar() now exits with an error when the value (including
K/M suffixes) overflows 32 bits, and compression level arguments
(-#, -e#, --fast=#) are rejected when larger than INT_MAX.
LZ4_CLEVEL and LZ4_NBWORKERS values that overflow are ignored with the
existing warning, like other invalid values.

Also fix the --fast=0 and --fast=-1 tests in test-lz4-basic.sh: they
used a misspelled file name (dg20K instead of dg20k), so lz4 failed
because the file was missing rather than because of the option.
2026-10-18 11:19:49 -07:00
Yann Collet
830fc63f80 lz4: LZ4_decompress_generic(): simplify the last literals copy
The safe loop's last literals path took about 50 lines, with 8 DEBUGLOG
and 6 comment blocks, for two rules: partialDecoding clamps the literal
length to the input and output left, otherwise the literals must end
exactly at input end, within output. State them directly, with MIN().

No behavior change: full-block decoders compile to identical code.
2026-10-18 11:06:23 -07:00
arshiya tabasum
e829d4b3df lz4: reject negative dictSize in dictionary entry points
LZ4_decompress_safe_usingDict(), LZ4_decompress_safe_partial_usingDict(),
LZ4_decompress_fast_usingDict() and LZ4_setStreamDecode() only assert()
that dictSize is not negative, then cast it to size_t. In release builds
the dictionary becomes ~SIZE_MAX bytes long, so every match offset is
accepted and decoding reads before the dictionary buffer.

LZ4_loadDictHC() has the same assert(): on lz4mid levels the fill target
of LZ4MID_fillHTable() wraps and it reads up to 32 KB before the
dictionary.

The decoders now return -1, LZ4_setStreamDecode() returns 0 (its
documented error), and LZ4_loadDictHC() loads no dictionary, as
LZ4_loadDict() already does for a negative size.
2026-10-18 22:46:29 +05:30
Yann Collet
8af4c50033 Merge pull request #1888 from lz4/decompress_ll_ml
lz4: LZ4_decompress_generic(): name literal and match lengths ll and ml
2026-10-18 10:05:11 -07:00
Yann Collet
970fd67bb1 lz4: LZ4_decompress_generic(): name literal and match lengths ll and ml
A single variable `length` held the literal length, then the match length
token, then the full match length, so its meaning at a given point,
especially at safe_match_copy, had to be inferred from the control flow.
Use `ll` and `ml`, as LZ4_decompress_unsafe_generic() already does.

Also declare token, ll and cpy in each loop body, since they never outlive
an iteration: only match, offset and ml remain shared by both loops,
as the fast loop hands them over at safe_match_copy.

Renaming and scoping only: the generated code is identical.
2026-10-17 23:27:25 -07:00
Yann Collet
e7c3622341 Merge pull request #1886 from lz4/riscv_fast_dec_loop
enable LZ4_FAST_DEC_LOOP on riscv64 when unaligned accesses are fast
2026-10-17 23:10:46 -07:00
Yann Collet
de4314af9d Merge pull request #1887 from lz4/decompress_shortcut_flow
lz4: LZ4_decompress_generic() safe loop: remove the _copy_match label
2026-10-17 23:07:48 -07:00
Yann Collet
c6d0bf8f0e lz4: LZ4_decompress_generic() safe loop: remove the _copy_match label
When the second stage of the shortcut didn't work out, it jumped over
the normal literals path to _copy_match. Put the normal literals path
in the else branch instead: both branches decode the offset and match
length token, then join before the match length extension.

No behavior change: the combined output check of both shortcut stages
is kept, and the generated code runs the same number of instructions.
2026-10-17 19:04:35 -07:00
Yann Collet
74c02718d4 Merge pull request #1885 from lz4/decompress_nearEnd_copy
lz4: LZ4_decompress_generic() safe loop
2026-10-17 18:48:45 -07:00
Yann Collet
2343fa3e8e enable LZ4_FAST_DEC_LOOP on riscv64 when unaligned accesses are fast
The fast decoder loop relies on wide unaligned copies being cheap.
On RISC-V, compilers only assume so when __riscv_misaligned_fast is
defined (-mno-strict-align, or a -mcpu with fast unaligned accesses).
Otherwise, unaligned LZ4_memcpy() are compiled as byte copies,
and the fast loop nearly doubles them in LZ4_decompress_safe()
(clang 21 -O3, rv64gc: 147 -> 266 lbu/sb).
This is consistent with results reported in #1678, #1686 and #1739,
which enable the fast loop unconditionally on riscv64:
between -2.9% and +2.8% decompression speed on real data.

Default builds (rv64gc, rva23u64) are unchanged.
-mcpu=spacemit-x60 or -mno-strict-align now select the fast loop.
2026-10-17 18:32:49 -07:00
Yann Collet
0d39a4aa22 Merge pull request #1884 from lz4/readme_refresh
README: refresh text, links and build pointers
2026-10-17 18:25:45 -07:00
Yann Collet
2a997d35c9 lz4: LZ4_decompress_generic() safe loop: check the end of block once, then copy
Near the end of the block, the safe loop copied a match in 3 different ways:
the extDict branch with its own end-of-block check, a partialDecoding-only
branch with an exact copy, and the full decoding path, which mixed the
end-of-block error with a wild copy limited to oend-7 and a byte loop.

The safe loop now checks the end of block once (partialDecoding: truncate,
otherwise: error), then copies the match with LZ4_copyExtDictMatch(),
or close to the end with the new LZ4_copyMatchExact(), or else with
the wild copy, which no longer deals with the end of block.

LZ4_copyMatchExact() also replaces the byte loop of LZ4_copyExtDictMatch().
It copies an overlapping match by copying the whole repeated pattern,
doubling the distance up to 2 KB, instead of re-reading just written bytes,
so long repetitive matches decode faster (period 3: x16, zeros: +20%).
It is not inlined (new LZ4_FORCE_NOINLINE): its memcpy() calls slowed down
the hot loops of extDict decoders.
2026-10-17 18:19:45 -07:00
Yann Collet
bd733c1ae6 Merge pull request #1882 from lz4/decompress_fastloop_exits
lz4: LZ4_decompress_generic() fast loop hand over
2026-10-17 17:59:12 -07:00
Yann Collet
f42fdc171d README: refresh text, links and build pointers
- grammar: "a lossless compression algorithm", "multi-core CPUs",
  "The LZ4 library ... the BSD 2-Clause license", "defined in"
- clarify that the acceleration factor tunes compression speed
- dictionary API / CLI links were pinned to v1.8.3:
  point to the in-tree lib/lz4frame.h and programs/lz4.1.md instead
- Zstandard dictionary builder link: v1.3.5 -> dev
- mention the GPL-2.0-or-later license of the CLI, as stated in LICENSE
- switch homepage, zlib, zstd and Silesia links to https
- point to build/ for CMake, Meson and Visual Studio builds
- vcpkg instructions: ./vcpkg, not ./vcpkg.exe, after bootstrap-vcpkg.sh
- list the API headers in the Documentation section

build/README.md: VS2022 solution outputs to build\VS2022\bin, not build\VS2010\bin
2026-10-17 17:57:45 -07:00
Yann Collet
349d311573 Merge pull request #1883 from lz4/list_truncated_frames
--list on truncated frames
2026-10-17 17:01:08 -07:00
Yann Collet
72b82c1720 lz4: LZ4_decompress_safe_partial() clears the match segment of an invalid offset 0
Near the end of a partial decoding, a match with offset 0 was copied onto
itself, leaving previous dst content in decoded data. doc/lz4_Block_format.md
warns against it (information disclosure), and other decoding paths
already produce 0 bytes. Clear the match segment instead.
2026-10-17 16:33:40 -07:00
Yann Collet
7577ff1393 test --list on a truncated frame, add NEWS line
Removing the 4-byte content checksum from a valid frame must make
`lz4 --list` fail. Before the previous commit, the checksum was skipped
with fseek(), which succeeds past the end of file, so --list reported
the truncated frame as valid (exit 0) while `lz4 -t` rejected it.

The CLI is not covered by the fuzzers; this test runs in milliseconds.
2026-10-17 16:23:15 -07:00
jmestwa-coder
b65a5cc90a reject truncated frames in --list by validating full reads 2026-10-17 16:22:29 -07:00
Yann Collet
b158fc1a2b lz4: LZ4_decompress_generic() fast loop hands over at a sequence start
The fast loop jumped into the safe loop at 3 places, each expecting
a different partially decoded state. Two of them happen before anything
is written for the current sequence: near the end of input, and for long
literals too close to the end for a wild copy. In both cases, rewind ip
to the token, and let the safe loop restart the sequence.

This removes the labels safe_literal_copy_early and safe_literal_copy.
The only remaining entry in the middle of a sequence is safe_match_copy,
with a fully decoded match.

Also comment the state expected at each remaining label and the role of
the safe loop, and remove two comments outdated since decompress_fast*()
left this function.
2026-10-17 16:20:00 -07:00
Yann Collet
62916c67ec Merge pull request #1768 from lz4/dependabot/github_actions/actions/checkout-7.0.0
build(deps): bump actions/checkout from 6.0.2 to 7.0.1
2026-10-17 16:10:37 -07:00
Yann Collet
36306a14a0 Merge pull request #1881 from lz4/fix_multifile_suffix_size
lz4io: size multi-file output names for the suffix
2026-10-17 15:56:45 -07:00
dependabot[bot]
7c4d9817a5 build(deps): bump actions/checkout from 6.0.2 to 7.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...3d3c42e5aa)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 22:44:35 +00:00