:: commit 42eb5f97f33c13ff5765bca9f0170e129d22d582

Kamila Szewczyk <k@iczelia.net> — 2026-04-10 21:22

parents: c41bc7fa98

decompressor: gzip/tinf -> limlz

removes external dependency on tinf by replacing the compression algorithm with a simpler, faster, smaller and more auditable fixed-width LZ77 encoding purpose-tailored to x86 code mixed with data.

before: decompressor.bin 2,492 bytes (tinf dependency) with .text 0x875 and .rodata 0x13c bytes each.
after: decompressor.bin consists only of .text, 0xe6-byte decompressor; 90.8% reduction in decompressor volume.

the dependency on gzip during compile-time is replaced by host/limlzpack.c, a Lempel-Ziv encoder in 275 SLoC that uses a suffix array matchfinder (prefix-doubling in mathcal O(n log^2 n) and Storer-Szymanski backwards parse. the fixed-width formats packets as [F][LLLL][MMM], favouring a literal-skewed distribution with F switching between one-byte and two-byte offsets (favouring recent statistics).

integrity checking is done via crc32 with the polynomial 0xEDB88320, reflected.

the effective loss in compression ratio by using a tremendously simpler and less packed with edge cases algorithm causes a compression ratio hit well below 1KB, factoring in the stub sizes.

also adds new machinery for host cc detection per review.
diff --git a/.gitignore b/.gitignore
index bbecc3fe..73911602 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,7 @@
 *.exe
 *.EFI
 *.bin
-*.bin.gz
+*.bin.limlz
 *.tar*
 *.elf
 *.hdd
@@ -32,10 +32,8 @@
 /common/lib/stb_image.h
 /common/cc-runtime.s2.c
 /cc-runtime
-/decompressor/tinf
 /decompressor/cc-runtime.c
 /libfdt
-/tinf
 /edk2-ovmf
 /bochsout.txt
 /bx_enh_dbg.ini
diff --git a/3RDPARTY.md b/3RDPARTY.md
index 1ab469c3..4a255511 100644
--- a/3RDPARTY.md
+++ b/3RDPARTY.md
@@ -49,9 +49,6 @@ below) provides headers and build-time support for UEFI.
     in case of installed copies, assuming the file has not been otherwise
     removed by the packager.
 
-- [tinf](https://github.com/jibsen/tinf) (Zlib) is used in early x86 BIOS
-stages for GZIP decompression of stage2.
-
 - [Flanterm](https://github.com/Mintsuki/Flanterm) (BSD-2-Clause) is used for
 text related screen drawing.
 
diff --git a/GNUmakefile.in b/GNUmakefile.in
index a7f515c2..d26fb01d 100644
--- a/GNUmakefile.in
+++ b/GNUmakefile.in
@@ -54,6 +54,8 @@ AWK := @AWK@
 export AWK
 
 CC := @CC@
+CC_FOR_BUILD := @CC_FOR_BUILD@
+CFLAGS_FOR_BUILD := @CFLAGS_FOR_BUILD@
 
 CPPFLAGS := @CPPFLAGS@
 CFLAGS := @CFLAGS@
@@ -185,7 +187,7 @@ uninstall:
 	rm -f '$(call SHESCAPE,$(DESTDIR)$(bindir))/limine'
 	rm -rf '$(call SHESCAPE,$(DESTDIR)$(datarootdir))/limine'
 
-$(call MKESCAPE,$(BUILDDIR))/stage1.stamp: $(STAGE1_FILES) $(call MKESCAPE,$(BUILDDIR))/decompressor-build/decompressor.bin $(call MKESCAPE,$(BUILDDIR))/common-bios/stage2.bin.gz
+$(call MKESCAPE,$(BUILDDIR))/stage1.stamp: $(STAGE1_FILES) $(call MKESCAPE,$(BUILDDIR))/decompressor-build/decompressor.bin $(call MKESCAPE,$(BUILDDIR))/common-bios/stage2.bin.limlz
 	$(MKDIR_P) '$(call SHESCAPE,$(BINDIR))'
 	cd '$(call SHESCAPE,$(SRCDIR))/stage1/hdd' && nasm bootsect.asm -Wall -w-unknown-warning -w-reloc $(WERROR_FLAG) -fbin -DBUILDDIR="'"'$(call NASMESCAPE,$(BUILDDIR))'"'" -o '$(call SHESCAPE,$(BINDIR))/limine-bios-hdd.bin'
 ifneq ($(BUILD_BIOS_CD),no)
@@ -301,7 +303,6 @@ dist:
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/picoefi/.gitignore"
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/cc-runtime"
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/libfdt/.git"
-	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/tinf"
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/common/lib/stb_image.h.nopatch"
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/.git"
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))'/"$(DIST_OUTPUT)/.gitignore"
@@ -327,7 +328,7 @@ distclean: clean
 
 .PHONY: maintainer-clean
 maintainer-clean: distclean
-	cd '$(call SHESCAPE,$(SRCDIR))' && rm -rf flanterm common/lib/stb_image.h.nopatch common/lib/stb_image.h decompressor/tinf tinf libfdt freestnd-c-hdrs cc-runtime common/cc-runtime.s2.c decompressor/cc-runtime.c limine-protocol picoefi configure timestamps build-aux *'~' autom4te.cache aclocal.m4 *.tar*
+	cd '$(call SHESCAPE,$(SRCDIR))' && rm -rf flanterm common/lib/stb_image.h.nopatch common/lib/stb_image.h libfdt freestnd-c-hdrs cc-runtime common/cc-runtime.s2.c decompressor/cc-runtime.c limine-protocol picoefi configure timestamps build-aux *'~' autom4te.cache aclocal.m4 *.tar*
 
 .PHONY: common-uefi-x86-64
 common-uefi-x86-64:
@@ -380,15 +381,20 @@ common-uefi-ia32-clean:
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))/common-uefi-ia32'
 
 .PHONY: common-bios
-common-bios:
+common-bios: $(call MKESCAPE,$(BINDIR))/limlzpack
 	$(MAKE) -C '$(call SHESCAPE,$(SRCDIR))/common' -f common.mk \
 		TARGET=bios \
-		BUILDDIR='$(call SHESCAPE,$(BUILDDIR))/common-bios'
+		BUILDDIR='$(call SHESCAPE,$(BUILDDIR))/common-bios' \
+		LIMLZPACK='$(call SHESCAPE,$(BINDIR))/limlzpack'
 
 .PHONY: common-bios-clean
 common-bios-clean:
 	rm -rf '$(call SHESCAPE,$(BUILDDIR))/common-bios'
 
+$(call MKESCAPE,$(BINDIR))/limlzpack: $(call MKESCAPE,$(SRCDIR))/tools/limlzpack.c
+	$(MKDIR_P) '$(call SHESCAPE,$(BINDIR))'
+	$(CC_FOR_BUILD) $(CFLAGS_FOR_BUILD) -std=c99 -Wall -Wextra $(WERROR_FLAG) '$(call SHESCAPE,$<)' -o '$(call SHESCAPE,$@)'
+
 .PHONY: decompressor
 decompressor:
 	$(MAKE) -C '$(call SHESCAPE,$(SRCDIR))/decompressor' -f decompressor.mk \
diff --git a/INSTALL.md b/INSTALL.md
index 573b59f4..21307cff 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -7,7 +7,7 @@
 
 In order to build Limine, the following programs have to be installed:
 common UNIX tools (also known as `coreutils`),
-`GNU make`, `grep`, `sed`, `find`, `awk`, `gzip`, `nasm`, `mtools`
+`GNU make`, `grep`, `sed`, `find`, `awk`, `nasm`, `mtools`
 (optional, necessary to build `limine-uefi-cd.bin`).
 Furthermore, `gcc` or `llvm/clang` must also be installed, alongside
 the respective binutils.
diff --git a/bootstrap b/bootstrap
index 051425b1..591e6a0a 100755
--- a/bootstrap
+++ b/bootstrap
@@ -92,15 +92,6 @@ if ! test -f version; then
         picoefi \
         9c99f66e4c15aebfd72e7becb72358473b484fcf
 
-    clone_repo_commit \
-        https://github.com/jibsen/tinf.git \
-        tinf \
-        57ffa1f1d5e3dde19011b2127bd26d01689b694b
-    mkdir -p decompressor/tinf
-    cp tinf/src/tinf.h tinf/src/tinflate.c tinf/src/tinfgzip.c tinf/src/crc32.c decompressor/tinf/
-    patch -p0 < decompressor/tinf.patch
-    rm -f decompressor/tinf/*.orig
-
     clone_repo_commit \
         https://github.com/Mintsuki/Flanterm.git \
         flanterm \
diff --git a/common/common.mk b/common/common.mk
index 56f3f414..f44601ef 100644
--- a/common/common.mk
+++ b/common/common.mk
@@ -304,7 +304,7 @@ override HEADER_DEPS := $(addprefix $(call MKESCAPE,$(BUILDDIR))/, $(C_FILES:.c=
 .PHONY: all
 
 ifeq ($(TARGET),bios)
-all: $(call MKESCAPE,$(BUILDDIR))/limine-bios.sys $(call MKESCAPE,$(BUILDDIR))/stage2.bin.gz
+all: $(call MKESCAPE,$(BUILDDIR))/limine-bios.sys $(call MKESCAPE,$(BUILDDIR))/stage2.bin.limlz
 endif
 ifeq ($(TARGET),uefi-x86-64)
 all: $(call MKESCAPE,$(BUILDDIR))/BOOTX64.EFI
@@ -324,8 +324,8 @@ endif
 
 ifeq ($(TARGET),bios)
 
-$(call MKESCAPE,$(BUILDDIR))/stage2.bin.gz: $(call MKESCAPE,$(BUILDDIR))/stage2.bin
-	gzip -n -9 < '$(call SHESCAPE,$<)' > '$(call SHESCAPE,$@)'
+$(call MKESCAPE,$(BUILDDIR))/stage2.bin.limlz: $(call MKESCAPE,$(BUILDDIR))/stage2.bin $(LIMLZPACK)
+	'$(call SHESCAPE,$(LIMLZPACK))' '$(call SHESCAPE,$<)' '$(call SHESCAPE,$@)'
 
 $(call MKESCAPE,$(BUILDDIR))/stage2.bin: $(call MKESCAPE,$(BUILDDIR))/limine-bios.sys
 	dd if='$(call SHESCAPE,$<)' bs=$$(( 0x$$("$(READELF_FOR_TARGET)" -S '$(call SHESCAPE,$(BUILDDIR))/limine.elf' | $(GREP) '\.text\.stage3' | $(SED) 's/^.*] //' | $(AWK) '{print $$3}' | $(SED) 's/^0*//') - 0xf000 )) count=1 of='$(call SHESCAPE,$@)' 2>/dev/null
diff --git a/configure.ac b/configure.ac
index b2e5d3c4..0b70f5b7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3,6 +3,7 @@ AC_INIT([Limine], [m4_esyscmd([./version.sh])], [https://github.com/Limine-Bootl
 AC_PREREQ([2.69])
 
 AC_CONFIG_AUX_DIR([build-aux])
+AC_CONFIG_MACRO_DIRS([m4])
 
 SRCDIR="$(cd "$srcdir" && pwd -P)"
 BUILDDIR="$(pwd -P)"
@@ -17,6 +18,7 @@ AC_SUBST([SOURCE_DATE_EPOCH])
 AC_SUBST([SOURCE_DATE_EPOCH_TOUCH])
 
 AC_CANONICAL_HOST
+AC_CANONICAL_BUILD
 
 # Portably convert relative paths into absolute paths.
 rel2abs() {
@@ -52,6 +54,9 @@ AC_LANG([C])
 AC_PROG_CC
 CC="$(rel2abs "$CC")"
 
+AX_PROG_CC_FOR_BUILD
+CC_FOR_BUILD="$(rel2abs "$CC_FOR_BUILD")"
+
 werror_state="no"
 AC_ARG_ENABLE([werror],
     [AS_HELP_STRING([--enable-werror], [treat warnings as errors])],
@@ -208,7 +213,6 @@ if test "x$BUILD_BIOS" = "xno"; then
 else
     BUILD_BIOS="limine-bios"
     NEED_NASM=yes
-    NEED_GZIP=yes
 fi
 
 AC_SUBST([BUILD_BIOS])
@@ -311,12 +315,6 @@ if test "x$NEED_NASM" = "xyes"; then
     fi
 fi
 
-if test "x$NEED_GZIP" = "xyes"; then
-    AC_CHECK_PROG([GZIP_FOUND], [gzip], [yes])
-    if ! test "x$GZIP_FOUND" = "xyes"; then
-        AC_MSG_ERROR([gzip not found, please install gzip before configuring])
-    fi
-fi
 
 BORROWED_CFLAGS=""
 for cflag in $CFLAGS; do
diff --git a/decompressor/decompressor.asm b/decompressor/decompressor.asm
new file mode 100644
index 00000000..1155689a
--- /dev/null
+++ b/decompressor/decompressor.asm
@@ -0,0 +1,139 @@
+; limlz: Copyright (C) 2026 Kamila Szewczyk <k@iczelia.net>
+; limine: Copyright (C) 2019-2026 Mintsuki and contributors.
+; 
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions are met:
+; 
+; 1. Redistributions of source code must retain the above copyright notice, this
+;    list of conditions and the following disclaimer.
+; 
+; 2. Redistributions in binary form must reproduce the above copyright notice,
+;    this list of conditions and the following disclaimer in the documentation
+;    and/or other materials provided with the distribution.
+; 
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+; ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+; CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+; OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+bits 32
+
+section .entry progbits alloc exec nowrite align=16
+
+global _start
+_start:
+    cld
+    ; On stack (cdecl): [esp+4]=compressed_stage2, [esp+8]=stage2_size,
+    ;                   [esp+12]=boot_drive (byte), [esp+16]=pxe
+    mov    ebx, dword [esp+0x4]      ; compressed_stage2
+    mov    ebp, dword [ebx]          ; expected_crc = *(uint32_t *)compressed_stage2
+    lea    edx, [ebx+0x4]            ; ip = compressed_stage2 + 4
+    add    ebx, dword [esp+0x8]      ; ipe = compressed_stage2 + stage2_size
+    mov    edi, 0xf000               ; op = dest
+    ; LZ decompression loop
+.Ltoken:
+    movzx  ecx, byte [edx]
+    lea    esi, [edx+0x1]
+    mov    eax, ecx                  ; save token
+    shr    ecx, 0x3
+    and    ecx, 0xf                  ; literal length = (token >> 3) & 15
+    cmp    ecx, 0xf
+    jne    .Llitcopy
+    movzx  ecx, byte [edx+0x1]
+    lea    esi, [edx+0x2]
+    add    ecx, 0xf                  ; length += extra byte + 15
+.Llitcopy:
+    rep    movsb                     ; copy literals
+    cmp    esi, ebx
+    jae    .Lcrc                     ; if ip >= ipe, done
+    test   al, al
+    jns    .Loffset1                 ; bit 7 clear => 1-byte offset
+    lea    edx, [esi+0x2]
+    movzx  esi, word [esi]           ; 2-byte offset
+    jmp    .Lmatchlen
+.Loffset1:
+    lea    edx, [esi+0x1]
+    movzx  esi, byte [esi]           ; 1-byte offset
+.Lmatchlen:
+    and    al, 0x7
+    cmp    al, 0x7
+    je     .Lmatchextra
+    movzx  eax, al
+    jmp    .Ldomatch
+.Lmatchextra:
+    movzx  eax, byte [edx]
+    inc    edx
+    add    eax, 0x7                  ; matchlen += extra byte + 7
+.Ldomatch:
+    mov    ecx, edi
+    sub    ecx, esi                  ; match = op - offset
+    mov    esi, ecx
+    lea    ecx, [eax+0x4]            ; count = matchlen + 4
+    rep    movsb                     ; copy match
+    jmp    .Ltoken
+    ; CRC32 verification
+.Lcrc:
+    mov    edx, 0xf000               ; ptr = dest
+    mov    esi, edx                  ; (also reused for esp later)
+    xor    eax, eax
+    dec    eax
+.Lcrc_byte:
+    cmp    edx, edi
+    je     .Lcrc_done
+    lea    ecx, [edx+0x1]
+    movzx  edx, byte [edx]
+    xor    eax, edx
+    push   0x08
+    pop    edx                       ; 8 bits per byte
+.Lcrc_bit:
+    mov    ebx, eax
+    and    eax, 0x1
+    shr    ebx, 1
+    neg    eax
+    and    eax, 0xedb88320
+    xor    eax, ebx
+    dec    edx
+    jne    .Lcrc_bit
+    mov    edx, ecx
+    jmp    .Lcrc_byte
+.Lcrc_done:
+    not    eax
+    cmp    eax, ebp
+    jne    .Lerror
+    ; Jump to decompressed stage2
+    movzx  eax, byte [esp+0xc]       ; boot_drive
+    mov    ecx, dword [esp+0x10]     ; pxe
+    mov    esp, esi
+    xor    ebp, ebp
+    push   ecx
+    push   eax
+    push   ebp
+    push   esi
+    ret                              ; jump to 0xf000
+    ; Error: display message and cli/hlt
+.Lerror:
+    mov    edx, errmsg
+    mov    eax, 0xb8000
+.Lerror_loop:
+    movzx  ecx, byte [edx]
+    add    eax, 0x2
+    inc    edx
+    or     ch, 0x4f
+    mov    word [eax-0x2], cx
+    cmp    eax, 0xB8000 + errmsg.len * 2
+    jne    .Lerror_loop
+    cli
+    hlt
+
+section .rodata progbits alloc noexec nowrite align=1
+
+errmsg: db "limine integrity error"
+.len: equ $ - errmsg - 1
+
+section .note.GNU-stack noalloc noexec nowrite progbits
diff --git a/decompressor/decompressor.mk b/decompressor/decompressor.mk
index 718d07f3..1e6a3a43 100644
--- a/decompressor/decompressor.mk
+++ b/decompressor/decompressor.mk
@@ -39,7 +39,6 @@ override CFLAGS_FOR_TARGET += \
 
 override CPPFLAGS_FOR_TARGET := \
     -I . \
-    -I tinf \
     -isystem ../freestnd-c-hdrs/include \
     $(CPPFLAGS_FOR_TARGET) \
     -MMD \
diff --git a/decompressor/entry.asm b/decompressor/entry.asm
deleted file mode 100644
index 70aa0cfc..00000000
--- a/decompressor/entry.asm
+++ /dev/null
@@ -1,20 +0,0 @@
-extern bss_begin
-extern bss_end
-extern entry
-
-section .entry progbits alloc exec nowrite align=16
-
-global _start
-_start:
-    cld
-
-    ; Zero out .bss
-    xor al, al
-    mov edi, bss_begin
-    mov ecx, bss_end
-    sub ecx, bss_begin
-    rep stosb
-
-    jmp entry
-
-section .note.GNU-stack noalloc noexec nowrite progbits
diff --git a/decompressor/main.c b/decompressor/main.c
deleted file mode 100644
index 4bea9300..00000000
--- a/decompressor/main.c
+++ /dev/null
@@ -1,37 +0,0 @@
-#include <stdint.h>
-#include <stddef.h>
-#include <stdnoreturn.h>
-#include <tinf.h>
-
-noreturn void entry(uint8_t *compressed_stage2, size_t stage2_size, uint8_t boot_drive, int pxe) {
-    // The decompressor should decompress compressed_stage2 to address 0xf000.
-    // The output buffer extends up to 0x70000 where the decompressor itself lives.
-    uint8_t *dest = (uint8_t *)0xf000;
-    unsigned int destLen = 0x70000 - 0xf000;
-
-    if (tinf_gzip_uncompress(dest, &destLen, compressed_stage2, stage2_size) != 0) {
-        const char *msg = "Limine decomp error";
-        volatile uint16_t *vga = (volatile uint16_t *)0xB8000;
-        for (size_t i = 0; msg[i]; i++) {
-            vga[i] = 0x4F00 | (uint8_t)msg[i];
-        }
-        for (;;) {
-            asm volatile ("cli; hlt");
-        }
-    }
-
-    asm volatile (
-        "movl $0xf000, %%esp\n\t"
-        "xorl %%ebp, %%ebp\n\t"
-        "pushl %1\n\t"
-        "pushl %0\n\t"
-        "pushl $0\n\t"
-        "pushl $0xf000\n\t"
-        "ret\n\t"
-        :
-        : "r" ((uint32_t)boot_drive), "r" (pxe)
-        : "memory"
-    );
-
-    __builtin_unreachable();
-}
diff --git a/decompressor/memory.c b/decompressor/memory.c
deleted file mode 100644
index a5353b2b..00000000
--- a/decompressor/memory.c
+++ /dev/null
@@ -1,53 +0,0 @@
-#include <stdint.h>
-#include <stddef.h>
-
-void *memcpy(void *restrict dest, const void *restrict src, size_t n) {
-    uint8_t *restrict pdest = (uint8_t *restrict)dest;
-    const uint8_t *restrict psrc = (const uint8_t *restrict)src;
-
-    for (size_t i = 0; i < n; i++) {
-        pdest[i] = psrc[i];
-    }
-
-    return dest;
-}
-
-void *memset(void *s, int c, size_t n) {
-    uint8_t *p = (uint8_t *)s;
-
-    for (size_t i = 0; i < n; i++) {
-        p[i] = (uint8_t)c;
-    }
-
-    return s;
-}
-
-void *memmove(void *dest, const void *src, size_t n) {
-    uint8_t *pdest = (uint8_t *)dest;
-    const uint8_t *psrc = (const uint8_t *)src;
-
-    if ((uintptr_t)src > (uintptr_t)dest) {
-        for (size_t i = 0; i < n; i++) {
-            pdest[i] = psrc[i];
-        }
-    } else if ((uintptr_t)src < (uintptr_t)dest) {
-        for (size_t i = n; i > 0; i--) {
-            pdest[i-1] = psrc[i-1];
-        }
-    }
-
-    return dest;
-}
-
-int memcmp(const void *s1, const void *s2, size_t n) {
-    const uint8_t *p1 = (const uint8_t *)s1;
-    const uint8_t *p2 = (const uint8_t *)s2;
-
-    for (size_t i = 0; i < n; i++) {
-        if (p1[i] != p2[i]) {
-            return p1[i] < p2[i] ? -1 : 1;
-        }
-    }
-
-    return 0;
-}
diff --git a/decompressor/tinf.patch b/decompressor/tinf.patch
deleted file mode 100644
index d955918e..00000000
--- a/decompressor/tinf.patch
+++ /dev/null
@@ -1,54 +0,0 @@
---- tinf-clean/tinfgzip.c	2026-03-31 13:52:17.360241095 +0200
-+++ decompressor/tinf/tinfgzip.c	2026-03-31 14:00:21.885490126 +0200
-@@ -101,7 +101,7 @@
- 	/* Skip file name if present */
- 	if (flg & FNAME) {
- 		do {
--			if (start - src >= sourceLen) {
-+			if (((unsigned int)(start - src)) >= sourceLen) {
- 				return TINF_DATA_ERROR;
- 			}
- 		} while (*start++);
-@@ -110,7 +110,7 @@
- 	/* Skip file comment if present */
- 	if (flg & FCOMMENT) {
- 		do {
--			if (start - src >= sourceLen) {
-+			if (((unsigned int)(start - src)) >= sourceLen) {
- 				return TINF_DATA_ERROR;
- 			}
- 		} while (*start++);
-@@ -120,7 +120,7 @@
- 	if (flg & FHCRC) {
- 		unsigned int hcrc;
- 
--		if (start - src > sourceLen - 2) {
-+		if ((unsigned int)(start - src) > sourceLen - 2) {
- 			return TINF_DATA_ERROR;
- 		}
- 
---- tinf-clean/tinflate.c	2026-03-31 13:52:17.360241095 +0200
-+++ decompressor/tinf/tinflate.c	2026-03-31 14:03:04.603900859 +0200
-@@ -25,7 +25,7 @@
- 
- #include "tinf.h"
- 
--#include <assert.h>
-+#define assert(...)
- #include <limits.h>
- 
- #if defined(UINT_MAX) && (UINT_MAX) < 0xFFFFFFFFUL
-@@ -501,11 +501,11 @@
- 
- 	d->source += 4;
- 
--	if (d->source_end - d->source < length) {
-+	if ((unsigned int)(d->source_end - d->source) < length) {
- 		return TINF_DATA_ERROR;
- 	}
- 
--	if (d->dest_end - d->dest < length) {
-+	if ((unsigned int)(d->dest_end - d->dest) < length) {
- 		return TINF_BUF_ERROR;
- 	}
- 
diff --git a/m4/ax_prog_cc_for_build.m4 b/m4/ax_prog_cc_for_build.m4
new file mode 100644
index 00000000..4d1de993
--- /dev/null
+++ b/m4/ax_prog_cc_for_build.m4
@@ -0,0 +1,175 @@
+# ===========================================================================
+#   https://www.gnu.org/software/autoconf-archive/ax_prog_cc_for_build.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_PROG_CC_FOR_BUILD
+#
+# DESCRIPTION
+#
+#   This macro searches for a C compiler that generates native executables,
+#   that is a C compiler that surely is not a cross-compiler. This can be
+#   useful if you have to generate source code at compile-time like for
+#   example GCC does.
+#
+#   The macro sets the CC_FOR_BUILD and CPP_FOR_BUILD macros to anything
+#   needed to compile or link (CC_FOR_BUILD) and preprocess (CPP_FOR_BUILD).
+#   The value of these variables can be overridden by the user by specifying
+#   a compiler with an environment variable (like you do for standard CC).
+#
+#   It also sets BUILD_EXEEXT and BUILD_OBJEXT to the executable and object
+#   file extensions for the build platform, and GCC_FOR_BUILD to `yes' if
+#   the compiler we found is GCC. All these variables but GCC_FOR_BUILD are
+#   substituted in the Makefile.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Paolo Bonzini <bonzini@gnu.org>
+#
+#   Copying and distribution of this file, with or without modification, are
+#   permitted in any medium without royalty provided the copyright notice
+#   and this notice are preserved. This file is offered as-is, without any
+#   warranty.
+
+#serial 26
+
+AU_ALIAS([AC_PROG_CC_FOR_BUILD], [AX_PROG_CC_FOR_BUILD])
+AC_DEFUN([AX_PROG_CC_FOR_BUILD], [dnl
+AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_PROG_CPP])dnl
+AC_REQUIRE([AC_CANONICAL_BUILD])dnl
+
+dnl Use the standard macros, but make them use other variable names
+dnl
+pushdef([ac_cv_prog_CPP], ac_cv_build_prog_CPP)dnl
+pushdef([ac_cv_prog_gcc], ac_cv_build_prog_gcc)dnl
+pushdef([ac_cv_prog_cc_c89], ac_cv_build_prog_cc_c89)dnl
+pushdef([ac_cv_prog_cc_c99], ac_cv_build_prog_cc_c99)dnl
+pushdef([ac_cv_prog_cc_c11], ac_cv_build_prog_cc_c11)dnl
+pushdef([ac_cv_prog_cc_c23], ac_cv_build_prog_cc_c23)dnl
+pushdef([ac_cv_prog_cc_stdc], ac_cv_build_prog_cc_stdc)dnl
+pushdef([ac_cv_prog_cc_works], ac_cv_build_prog_cc_works)dnl
+pushdef([ac_cv_prog_cc_cross], ac_cv_build_prog_cc_cross)dnl
+pushdef([ac_cv_prog_cc_g], ac_cv_build_prog_cc_g)dnl
+pushdef([ac_prog_cc_stdc], ac_build_prog_cc_stdc)dnl
+pushdef([ac_exeext], ac_build_exeext)dnl
+pushdef([ac_objext], ac_build_objext)dnl
+pushdef([CC], CC_FOR_BUILD)dnl
+pushdef([CPP], CPP_FOR_BUILD)dnl
+pushdef([GCC], GCC_FOR_BUILD)dnl
+pushdef([CFLAGS], CFLAGS_FOR_BUILD)dnl
+pushdef([CPPFLAGS], CPPFLAGS_FOR_BUILD)dnl
+pushdef([LDFLAGS], LDFLAGS_FOR_BUILD)dnl
+pushdef([host], build)dnl
+pushdef([host_alias], build_alias)dnl
+pushdef([host_cpu], build_cpu)dnl
+pushdef([host_vendor], build_vendor)dnl
+pushdef([host_os], build_os)dnl
+pushdef([ac_cv_host], ac_cv_build)dnl
+pushdef([ac_cv_host_alias], ac_cv_build_alias)dnl
+pushdef([ac_cv_host_cpu], ac_cv_build_cpu)dnl
+pushdef([ac_cv_host_vendor], ac_cv_build_vendor)dnl
+pushdef([ac_cv_host_os], ac_cv_build_os)dnl
+pushdef([ac_tool_prefix], ac_build_tool_prefix)dnl
+pushdef([am_cv_CC_dependencies_compiler_type], am_cv_build_CC_dependencies_compiler_type)dnl
+pushdef([am_cv_prog_cc_c_o], am_cv_build_prog_cc_c_o)dnl
+pushdef([cross_compiling], cross_compiling_build)dnl
+dnl
+dnl These variables are problematic to rename by M4 macros, so we save
+dnl their values in alternative names, and restore the values later.
+dnl
+dnl _AC_COMPILER_EXEEXT and _AC_COMPILER_OBJEXT internally call
+dnl AC_SUBST which prevents the renaming of EXEEXT and OBJEXT
+dnl variables. It's not a good idea to rename ac_cv_exeext and
+dnl ac_cv_objext either as they're related.
+dnl Renaming ac_exeext and ac_objext is safe though.
+dnl
+ac_cv_host_exeext=$ac_cv_exeext
+AS_VAR_SET_IF([ac_cv_build_exeext],
+  [ac_cv_exeext=$ac_cv_build_exeext],
+  [AS_UNSET([ac_cv_exeext])])
+ac_cv_host_objext=$ac_cv_objext
+AS_VAR_SET_IF([ac_cv_build_objext],
+  [ac_cv_objext=$ac_cv_build_objext],
+  [AS_UNSET([ac_cv_objext])])
+dnl
+dnl ac_cv_c_compiler_gnu is used in _AC_LANG_COMPILER_GNU (called by
+dnl AC_PROG_CC) indirectly.
+dnl
+ac_cv_host_c_compiler_gnu=$ac_cv_c_compiler_gnu
+AS_VAR_SET_IF([ac_cv_build_c_compiler_gnu],
+  [ac_cv_c_compiler_gnu=$ac_cv_build_c_compiler_gnu],
+  [AS_UNSET([ac_cv_c_compiler_gnu])])
+
+cross_compiling_build=no
+
+ac_build_tool_prefix=
+AS_IF([test -n "$build"],      [ac_build_tool_prefix="$build-"],
+      [test -n "$build_alias"],[ac_build_tool_prefix="$build_alias-"])
+
+AC_LANG_PUSH([C])
+AC_PROG_CC
+_AC_COMPILER_EXEEXT
+_AC_COMPILER_OBJEXT
+AC_PROG_CPP
+
+BUILD_EXEEXT=$ac_cv_exeext
+BUILD_OBJEXT=$ac_cv_objext
+
+dnl Restore the old definitions
+dnl
+popdef([cross_compiling])dnl
+popdef([am_cv_prog_cc_c_o])dnl
+popdef([am_cv_CC_dependencies_compiler_type])dnl
+popdef([ac_tool_prefix])dnl
+popdef([ac_cv_host_os])dnl
+popdef([ac_cv_host_vendor])dnl
+popdef([ac_cv_host_cpu])dnl
+popdef([ac_cv_host_alias])dnl
+popdef([ac_cv_host])dnl
+popdef([host_os])dnl
+popdef([host_vendor])dnl
+popdef([host_cpu])dnl
+popdef([host_alias])dnl
+popdef([host])dnl
+popdef([LDFLAGS])dnl
+popdef([CPPFLAGS])dnl
+popdef([CFLAGS])dnl
+popdef([GCC])dnl
+popdef([CPP])dnl
+popdef([CC])dnl
+popdef([ac_objext])dnl
+popdef([ac_exeext])dnl
+popdef([ac_prog_cc_stdc])dnl
+popdef([ac_cv_prog_cc_g])dnl
+popdef([ac_cv_prog_cc_cross])dnl
+popdef([ac_cv_prog_cc_works])dnl
+popdef([ac_cv_prog_cc_stdc])dnl
+popdef([ac_cv_prog_cc_c23])dnl
+popdef([ac_cv_prog_cc_c11])dnl
+popdef([ac_cv_prog_cc_c99])dnl
+popdef([ac_cv_prog_cc_c89])dnl
+popdef([ac_cv_prog_gcc])dnl
+popdef([ac_cv_prog_CPP])dnl
+dnl
+ac_cv_exeext=$ac_cv_host_exeext
+EXEEXT=$ac_cv_host_exeext
+ac_cv_objext=$ac_cv_host_objext
+OBJEXT=$ac_cv_host_objext
+ac_cv_c_compiler_gnu=$ac_cv_host_c_compiler_gnu
+ac_compiler_gnu=$ac_cv_host_c_compiler_gnu
+
+dnl restore global variables ac_ext, ac_cpp, ac_compile,
+dnl ac_link, ac_compiler_gnu (dependent on the current
+dnl language after popping):
+AC_LANG_POP([C])
+
+dnl Finally, set Makefile variables
+dnl
+AC_SUBST([BUILD_EXEEXT])dnl
+AC_SUBST([BUILD_OBJEXT])dnl
+AC_SUBST([CFLAGS_FOR_BUILD])dnl
+AC_SUBST([CPPFLAGS_FOR_BUILD])dnl
+AC_SUBST([LDFLAGS_FOR_BUILD])dnl
+])
diff --git a/stage1/cd/bootsect.asm b/stage1/cd/bootsect.asm
index 6170f395..073acc6a 100644
--- a/stage1/cd/bootsect.asm
+++ b/stage1/cd/bootsect.asm
@@ -100,7 +100,7 @@ incbin DECOMPRESSOR_PATH
 
 align 16
 stage2:
-%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.gz'
+%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.limlz'
 incbin STAGE2_PATH
 .size: equ $ - stage2
 
diff --git a/stage1/hdd/bootsect.asm b/stage1/hdd/bootsect.asm
index 0d2b47e1..c6b64d28 100644
--- a/stage1/hdd/bootsect.asm
+++ b/stage1/hdd/bootsect.asm
@@ -148,6 +148,6 @@ incbin DECOMPRESSOR_PATH
 
 align 16
 stage2:
-%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.gz'
+%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.limlz'
 incbin STAGE2_PATH
 .size: equ $ - stage2
diff --git a/stage1/pxe/bootsect.asm b/stage1/pxe/bootsect.asm
index c1b96fb3..e65370fd 100644
--- a/stage1/pxe/bootsect.asm
+++ b/stage1/pxe/bootsect.asm
@@ -54,7 +54,7 @@ incbin DECOMPRESSOR_PATH
 
 align 16
 stage2:
-%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.gz'
+%strcat STAGE2_PATH BUILDDIR, '/common-bios/stage2.bin.limlz'
 incbin STAGE2_PATH
 .size: equ $ - stage2
 .fullsize: equ $ - decompressor
diff --git a/tools/limlzpack.c b/tools/limlzpack.c
new file mode 100644
index 00000000..4fb0291c
--- /dev/null
+++ b/tools/limlzpack.c
@@ -0,0 +1,316 @@
+/* limlz: Copyright (C) 2026 Kamila Szewczyk <k@iczelia.net>
+ * limine: Copyright (C) 2019-2026 Mintsuki and contributors.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ 
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+typedef unsigned char byte;
+
+/*  Higher -> better compression with exponentally dimnishing gains.  */
+#define LIMLZ_SA_NEIGHBORS 32
+
+struct sa_cmp_ctx { int * rank; size_t n, k; };
+static struct sa_cmp_ctx g_sa_ctx;
+
+static int sa_cmp_idx(int i, int j) {
+  int ri, rj;
+  if (g_sa_ctx.rank[i] != g_sa_ctx.rank[j])
+    return g_sa_ctx.rank[i] - g_sa_ctx.rank[j];
+  ri = (i + (int)g_sa_ctx.k < (int)g_sa_ctx.n) ? g_sa_ctx.rank[i + g_sa_ctx.k] : -1;
+  rj = (j + (int)g_sa_ctx.k < (int)g_sa_ctx.n) ? g_sa_ctx.rank[j + g_sa_ctx.k] : -1;
+  return ri - rj;
+}
+
+static int sa_qsort_cmp(const void * a, const void * b) {
+  int i = *(const int *) a, j = *(const int *) b;
+  return sa_cmp_idx(i, j);
+}
+
+static int saca(const byte * s, size_t n, int * sa, int * rank, int * tmp) {
+  size_t i;
+  if (!n)
+    return 0;
+  for (i = 0; i < n; ++i) {
+    sa[i] = (int)i;  rank[i] = (int)s[i];
+  }
+  for (g_sa_ctx.k = 1;; g_sa_ctx.k <<= 1) {
+    g_sa_ctx.rank = rank;  g_sa_ctx.n = n;
+    qsort(sa, n, sizeof(sa[0]), sa_qsort_cmp);
+    tmp[sa[0]] = 0;
+    for (i = 1; i < n; ++i)
+      tmp[sa[i]] = tmp[sa[i - 1]] + (sa_cmp_idx(sa[i - 1], sa[i]) < 0);
+    for (i = 0; i < n; ++i)
+      rank[i] = tmp[i];
+    if ((size_t)rank[sa[n - 1]] == n - 1)
+      break;
+  }
+  return 0;
+}
+
+static size_t lcp_bytes(const byte * s, size_t n, size_t i, size_t j) {
+  size_t l = 0, m = n - (i > j ? i : j);
+  for (; l < m && s[i + l] == s[j + l]; ++l);
+  return l;
+}
+
+struct match_choice { uint32_t len;  uint16_t off; };
+struct parse_choice { uint32_t lit, mlen;  uint16_t off; };
+
+static int longest_matches(const byte * src, size_t n, struct match_choice * mch) {
+  int * sa, * rank, * tmp, * inv;
+  size_t i;
+  if (!n)
+    return 0;
+  sa = malloc(n * sizeof(*sa));
+  rank = malloc(n * sizeof(*rank));
+  tmp = malloc(n * sizeof(*tmp));
+  inv = malloc(n * sizeof(*inv));
+  if (!sa || !rank || !tmp || !inv || saca(src, n, sa, rank, tmp)) {
+    free(sa);  free(rank);  free(tmp);  free(inv);
+    return -1;
+  }
+  for (i = 0; i < n; ++i)
+    inv[sa[i]] = (int)i;
+  for (i = 0; i < n; ++i) {
+    int r = inv[i], d, rr;
+    size_t best_len = 0;
+    uint16_t best_off = 0;
+    for (d = -LIMLZ_SA_NEIGHBORS; d <= LIMLZ_SA_NEIGHBORS; ++d) {
+      size_t j, l, off;
+      if (!d)
+        continue;
+      rr = r + d;
+      if (rr < 0 || rr >= (int)n)
+        continue;
+      j = (size_t)sa[rr];
+      if (j >= i)
+        continue;
+      off = i - j;
+      if (off == 0 || off > 65535)
+        continue;
+      l = lcp_bytes(src, n, i, j);
+      if (l > best_len) {
+        best_len = l;
+        best_off = (uint16_t)off;
+      }
+    }
+    if (best_len >= 4) {
+      mch[i].len = (uint32_t)best_len;
+      mch[i].off = best_off;
+    } else {
+      mch[i].len = mch[i].off = 0;
+    }
+  }
+  free(sa);  free(rank);  free(tmp);  free(inv);
+  return 0;
+}
+
+static int encode_len_tail(byte ** outp, byte * out_end, size_t n) {
+  byte *out = * outp;
+  if (n >= 15) {
+    if (out >= out_end) return -1;
+    *out++ = (byte)(n - 15);
+  }
+  *outp = out;
+  return 0;
+}
+
+static int encode_len_tail_ml(byte ** outp, byte * out_end, size_t n) {
+  byte * out = *outp;
+  if (n >= 7) {
+    if (out >= out_end)
+      return -1;
+    *out++ = (byte)(n - 7);
+  }
+  *outp = out;
+  return 0;
+}
+
+static size_t limlzpack(void * dst, size_t dstcap, const void * srcv, size_t srcsz) {
+  const byte * src = (const byte *) srcv;
+  byte * dstp = (byte *) dst;
+  byte * out = dstp, * out_end = dstp + dstcap;
+  struct match_choice * mch, * bestm;
+  struct parse_choice * pick;
+  size_t i, * dp;
+  if (!srcsz) {
+    if (dstcap < 1)
+      return 0;
+    dstp[0] = 0;
+    return 1;
+  }
+  mch = calloc(srcsz, sizeof(*mch));
+  pick = calloc(srcsz + 1, sizeof(*pick));
+  bestm = calloc(srcsz, sizeof(*bestm));
+  dp = malloc((srcsz + 1) * sizeof(*dp));
+  if (!mch || !pick || !bestm || !dp || longest_matches(src, srcsz, mch))
+    goto fail;
+  dp[srcsz] = 0;
+  pick[srcsz].lit = pick[srcsz].mlen = pick[srcsz].off = 0;
+  for (i = srcsz; i-- > 0;) {
+    size_t j, best_cost;
+    uint32_t best_lit, best_len;
+    uint16_t best_off;
+    bestm[i].len = bestm[i].off = 0;
+    if (mch[i].len >= 4) {
+      size_t ml, lim = mch[i].len;
+      if (lim > 266) lim = 266;
+      size_t off_bytes = (mch[i].off > 255) ? 2 : 1;
+      size_t mcost = (size_t)-1;
+      uint32_t mlen = 0;
+      if (i + lim > srcsz)
+        lim = srcsz - i;
+      for (ml = 4; ml <= lim; ++ml) {
+        size_t c = off_bytes + (ml - 4 >= 7) + dp[i + ml];
+        if (c < mcost) {
+          mcost = c;  mlen = (uint32_t)ml;
+        }
+      }
+      if (mlen) {
+        bestm[i].len = mlen;  bestm[i].off = mch[i].off;
+      }
+    }
+    if (srcsz - i <= 270) { // 256 + 15 - 1
+      best_cost = 1 + (srcsz - i) + (srcsz - i >= 15);
+      best_lit = (uint32_t)(srcsz - i);
+    } else {
+      best_cost = (size_t)-1;
+      best_lit = 0;
+    }
+    best_len = best_off = 0;
+    for (j = i; j < srcsz && j - i <= 270; ++j) {
+      size_t lit = j - i, off_bytes_j, c;
+      if (!bestm[j].len)
+        continue;
+      off_bytes_j = (bestm[j].off > 255) ? 2 : 1;
+      c = 1 + lit + (lit >= 15) +
+                  (off_bytes_j + (bestm[j].len - 4 >= 7) + dp[j + bestm[j].len]);
+      if (c < best_cost) {
+        best_cost = c;  best_lit = (uint32_t)lit;
+        best_len = bestm[j].len;  best_off = bestm[j].off;
+      }
+    }
+    dp[i] = best_cost;  pick[i].lit = best_lit;
+    pick[i].mlen = best_len;  pick[i].off = best_off;
+  }
+  for (i = 0; i < srcsz; ) {
+    byte * tokenp;
+    size_t lit = pick[i].lit, ml = pick[i].mlen;
+    uint16_t off = pick[i].off;
+    unsigned token_hi, token_lo;
+    if (i + lit > srcsz)
+      goto fail;
+    if (i + lit < srcsz && ml < 4)
+      goto fail;
+    tokenp = out;
+    if (out >= out_end)
+      goto fail;
+    *out++ = 0;
+    token_hi = (lit < 15) ? (unsigned)lit : 15u;
+    if (encode_len_tail(&out, out_end, lit))
+      goto fail;
+    if ((size_t)(out_end - out) < lit)
+      goto fail;
+    memcpy(out, src + i, lit);
+    out += lit;
+    i += lit;
+    if (i >= srcsz) {
+      *tokenp = (byte)(token_hi << 3);
+      break;
+    }
+    unsigned mode_bit = (off > 255) ? 1u : 0u;
+    token_lo = (ml - 4 < 7) ? (unsigned)(ml - 4) : 7u;
+    *tokenp = (byte)((mode_bit << 7) | (token_hi << 3) | token_lo);
+    if (off > 255) {
+      if (out_end - out < 2)
+        goto fail;
+      *out++ = (byte)(off & 255);
+      *out++ = (byte)(off >> 8);
+    } else {
+      if (out >= out_end)
+        goto fail;
+      *out++ = (byte)off;
+    }
+    if (encode_len_tail_ml(&out, out_end, ml - 4))
+      goto fail;
+    i += ml;
+  }
+  free(mch);  free(pick);  free(bestm);  free(dp);
+  return (size_t)(out - dstp);
+fail:
+  free(mch);  free(pick);  free(bestm);  free(dp);
+  return 0;
+}
+
+static const uint32_t tab[16] = {
+  0x00000000u, 0x1DB71064u, 0x3B6E20C8u, 0x26D930ACu,
+  0x76DC4190u, 0x6B6B51F4u, 0x4DB26158u, 0x5005713Cu,
+  0xEDB88320u, 0xF00F9344u, 0xD6D6A3E8u, 0xCB61B38Cu,
+  0x9B64C2B0u, 0x86D3D2D4u, 0xA00AE278u, 0xBDBDF21Cu
+};
+
+static uint32_t crc32_nibble(const byte *data, size_t len) {
+  uint32_t crc = ~0u; // faster than decompressor.asm bit-by-bit, same result.
+  while (len--) {
+    crc ^= *data++;
+    crc = (crc >> 4) ^ tab[crc & 0x0Fu];
+    crc = (crc >> 4) ^ tab[crc & 0x0Fu];
+  }
+  return ~crc;
+}
+
+int main(int argc, char *argv[]) {
+  if (argc != 3) {
+    fprintf(stderr, "? %s <input> <output>\n", argv[0]);  return 1;
+  }
+  FILE * fin = fopen(argv[1], "rb");
+  FILE * fout = fopen(argv[2], "wb");
+  byte * inbuf, *outbuf;
+  size_t insz, outsz;
+  if (!fin || !fout) {
+    fprintf(stderr, "? fopen\n");  return 1;
+  }
+  fseek(fin, 0, SEEK_END);
+  insz = ftell(fin);
+  fseek(fin, 0, SEEK_SET);
+  inbuf = malloc(insz);  outbuf = malloc(insz * 2);
+  if (!inbuf || !outbuf) {
+    fprintf(stderr, "? malloc\n");  return 1;
+  }
+  fread(inbuf, 1, insz, fin);
+  fclose(fin);
+  outsz = limlzpack(outbuf, insz * 2, inbuf, insz);
+  if (!outsz) {
+    fprintf(stderr, "? limlzpack\n");  return 1;
+  }
+  uint32_t crc = crc32_nibble(inbuf, insz);
+  fwrite(&crc, sizeof(crc), 1, fout);
+  fwrite(outbuf, 1, outsz, fout);
+  fclose(fout);
+  free(inbuf);  free(outbuf);
+}
tab: 248 wrap: offon