/* unlzx.c - LZX archive extractor
 *
 * Original: version 1.0 (22.2.98), author unknown
 * Stdin support added: version 1.1 (03.4.01), Erik Meusel
 * Modernised to C11: 2026, style/safety pass
 *
 * LZX is a lossless compression format used primarily on the Amiga.
 * This tool extracts or lists the contents of .lzx archives.
 *
 * Build:
 *   gcc unlzx.c -o unlzx -O2 -Wall -Wextra -std=c11
 *
 * Usage:
 *   unlzx [-v|-x] [-c] [archive ...]
 *     -c  read from stdin
 *     -v  list archive contents
 *     -x  extract (default)
 */

/* _GNU_SOURCE expose tm_gmtoff dans struct tm (offset UTC DST-correct)
 * ainsi que les extensions POSIX nécessaires (getopt, etc.).
 * Doit être défini avant tout #include. */
#define _GNU_SOURCE

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

/* -------------------------------------------------------------------------
 * Constants & version
 * ---------------------------------------------------------------------- */

#define VERSION_STR  "$VER: unlzx 2.0 (2026)"
#define LZX_MAGIC_0  'L'
#define LZX_MAGIC_1  'Z'
#define LZX_MAGIC_2  'X'

#define READ_BUFFER_SIZE    16384u
/* decrunch buffer: 65536 working area + 258 bytes of overrun on each side */
#define DECRUNCH_BUF_SIZE   (258u + 65536u + 258u)
#define DECRUNCH_WINDOW     65536u

/* Archive header field sizes (all little-endian 32-bit unless noted) */
#define ARCHIVE_HEADER_SIZE  31
#define INFO_HEADER_SIZE     10
#define MAX_FILENAME_LEN     255
#define MAX_COMMENT_LEN      255

/* Pack modes */
#define PACK_STORE   0
#define PACK_NORMAL  2

/* Header flags */
#define HDR_FLAG_MERGED  0x01

/* File protection bits (Amiga style) */
#define HDR_PROT_READ    0x01
#define HDR_PROT_WRITE   0x02
#define HDR_PROT_DELETE  0x04
#define HDR_PROT_EXECUTE 0x08
#define HDR_PROT_ARCHIVE 0x10
#define HDR_PROT_HOLD    0x20
#define HDR_PROT_SCRIPT  0x40
#define HDR_PROT_PURE    0x80

/* -------------------------------------------------------------------------
 * Lookup tables (read-only, never modified at runtime)
 * ---------------------------------------------------------------------- */


/* CRC-32 table (standard polynomial 0xEDB88320, reflected) */
static const uint32_t crc_table[256] = {
    0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
    0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
    0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
    0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
    0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
    0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
    0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
    0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
    0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
    0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
    0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
    0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
    0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
    0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
    0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
    0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
    0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
    0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
    0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
    0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
    0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
    0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
    0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
    0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
    0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
    0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
    0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
    0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
    0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
    0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
    0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
    0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
    0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
    0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
    0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
    0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
    0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
    0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
    0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
    0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
    0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
    0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
    0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
    0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
    0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
    0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
    0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
    0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
    0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
    0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
    0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
    0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
    0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
    0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
    0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
    0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
    0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
    0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
    0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
    0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
    0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
    0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
    0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
    0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
};

/*
 * Huffman distance/length decode helper tables.
 *
 * table_one[i]  : number of extra bits to read for offset slot i
 * table_two[i]  : base offset for slot i
 * table_three[i]: bitmask for i extra bits  (2^i - 1)
 * table_four[i] : delta table used when updating literal lengths
 */
static const uint8_t table_one[32] = {
    0, 0, 0, 0, 1, 1, 2, 2, 3,  3,  4,  4,  5,  5,  6,  6,
    7, 7, 8, 8, 9, 9,10,10,11, 11, 12, 12, 13, 13, 14, 14
};

static const uint32_t table_two[32] = {
    0,     1,     2,     3,     4,     6,     8,    12,
   16,    24,    32,    48,    64,    96,   128,   192,
  256,   384,   512,   768,  1024,  1536,  2048,  3072,
 4096,  6144,  8192, 12288, 16384, 24576, 32768, 49152
};

static const uint32_t table_three[16] = {
    0,     1,     3,     7,    15,    31,    63,   127,
  255,   511,  1023,  2047,  4095,  8191, 16383, 32767
};

static const uint8_t table_four[34] = {
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16
};

/* -------------------------------------------------------------------------
 * Data structures
 * ---------------------------------------------------------------------- */

/* A single file entry within a (possibly merged) archive block. */
typedef struct filename_node {
    struct filename_node *next;
    uint32_t              length;   /* uncompressed size */
    uint32_t              crc;      /* expected CRC-32 of data */
    char                  filename[MAX_FILENAME_LEN + 1];
} filename_node_t;

/* All mutable state for one decompression session. Grouping it here
 * removes the original ~20 file-scope globals and makes the call chain
 * explicit. */
typedef struct decrunch_state {
    /* I/O buffers */
    uint8_t  read_buffer[READ_BUFFER_SIZE];
    uint8_t  decrunch_buffer[DECRUNCH_BUF_SIZE];

    uint8_t *src;           /* current read position in read_buffer */
    uint8_t *dst;           /* current write position in decrunch_buffer */
    uint8_t *src_end;       /* last safe source byte before refill needed */
    uint8_t *dst_end;       /* write limit for current decrunch pass */

    /* Bit-stream state */
    uint32_t control;
    int      shift;

    /* Decompression parameters */
    uint32_t decrunch_method;
    int32_t  decrunch_length; /* signed: we check <= 0 */
    uint32_t last_offset;
    uint32_t pack_size;
    uint32_t unpack_size;

    /* Huffman tables */
    uint8_t  offset_len[8];
    uint16_t offset_table[128];
    uint8_t  huffman20_len[20];
    uint16_t huffman20_table[96];
    uint8_t  literal_len[768];
    uint16_t literal_table[5120];

    /* CRC accumulator for the current file */
    uint32_t crc_sum;
} decrunch_state_t;

/* -------------------------------------------------------------------------
 * CRC-32 helpers
 * ---------------------------------------------------------------------- */

/* Update *crc_sum with len bytes starting at mem. */
static void crc_update(uint32_t *crc_sum, const uint8_t *mem, uint32_t len)
{
    if (!len) return;

    uint32_t tmp = ~(*crc_sum);
    do {
        tmp = crc_table[(*mem++ ^ tmp) & 0xFF] ^ (tmp >> 8);
    } while (--len);
    *crc_sum = ~tmp;
}

/* -------------------------------------------------------------------------
 * Huffman table builder
 * ---------------------------------------------------------------------- */

/*
 * Build a fast huffman decode table from per-symbol bit lengths.
 *
 * Returns true on success, false if the table is corrupt/incomplete.
 *
 * The table encodes short codes (≤ table_size bits) directly, and
 * uses chained tree nodes (stored in the upper portion of the same
 * array) for longer codes.
 */
static bool make_decode_table(int num_symbols, int table_size,
                               const uint8_t *length, uint16_t *table)
{
    uint32_t table_mask = (uint32_t)1 << table_size;
    uint32_t bit_mask   = table_mask >> 1;
    uint32_t pos        = 0;
    int      bit_num    = 1;
    bool     ok         = true;

    /* --- Phase 1: fill entries for codes up to table_size bits wide --- */
    while (ok && bit_num <= table_size) {
        for (int sym = 0; sym < num_symbols; sym++) {
            if (length[sym] != (uint8_t)bit_num) continue;

            /* Bit-reverse 'pos' to get the canonical table index. */
            uint32_t reverse = pos;
            uint32_t leaf    = 0;
            for (int fill = table_size; fill; fill--) {
                leaf    = (leaf << 1) | (reverse & 1);
                reverse >>= 1;
            }

            if ((pos += bit_mask) > table_mask) {
                ok = false;
                break;  /* table overflow */
            }

            /* Fill all entries that share the same prefix. */
            uint32_t next_sym = (uint32_t)1 << bit_num;
            for (uint32_t fill = bit_mask; fill; fill--) {
                table[leaf] = (uint16_t)sym;
                leaf += next_sym;
            }
        }
        bit_mask >>= 1;
        bit_num++;
    }

    /* --- Phase 2: codes longer than table_size bits (chained nodes) --- */
    if (ok && pos != table_mask) {
        /* Zero out the unused direct entries. */
        for (uint32_t sym = pos; sym < table_mask; sym++) {
            uint32_t reverse = sym;
            uint32_t leaf    = 0;
            for (int fill = table_size; fill; fill--) {
                leaf    = (leaf << 1) | (reverse & 1);
                reverse >>= 1;
            }
            table[leaf] = 0;
        }

        uint32_t next_sym  = table_mask >> 1;
        pos        <<= 16;
        table_mask <<= 16;
        bit_mask     = 32768;

        while (ok && bit_num <= 16) {
            for (int sym = 0; sym < num_symbols; sym++) {
                if (length[sym] != (uint8_t)bit_num) continue;

                uint32_t reverse = pos >> 16;
                uint32_t leaf    = 0;
                for (int fill = table_size; fill; fill--) {
                    leaf    = (leaf << 1) | (reverse & 1);
                    reverse >>= 1;
                }
                for (int fill = 0; fill < bit_num - table_size; fill++) {
                    if (table[leaf] == 0) {
                        table[next_sym * 2]     = 0;
                        table[next_sym * 2 + 1] = 0;
                        table[leaf] = (uint16_t)next_sym++;
                    }
                    leaf = (uint32_t)table[leaf] << 1;
                    leaf |= (pos >> (15 - fill)) & 1;
                }
                table[leaf] = (uint16_t)sym;

                if ((pos += bit_mask) > table_mask) {
                    ok = false;
                    break;
                }
            }
            bit_mask >>= 1;
            bit_num++;
        }
    }

    if (pos != table_mask) ok = false;
    return ok;
}

/* -------------------------------------------------------------------------
 * Literal table reader
 * ---------------------------------------------------------------------- */

/*
 * Read the decrunch method, decrunch length, and the huffman literal
 * table from the compressed bit-stream.
 *
 * Returns true on success.
 */
static bool read_literal_table(decrunch_state_t *s)
{
    uint32_t control = s->control;
    int      shift   = s->shift;

/* Helper macro: refill the 32-bit control word when shift goes negative.
 * The bit-stream is packed little-endian 16-bit words. */
#define REFILL() do {                                           \
    if (shift < 0) {                                           \
        shift   += 16;                                         \
        control += (uint32_t)(*s->src++) << (8 + shift);      \
        control += (uint32_t)(*s->src++) << shift;             \
    }                                                          \
} while (0)

#define CONSUME(n) do { control >>= (n); shift -= (n); REFILL(); } while (0)

    REFILL();

    s->decrunch_method = control & 7;
    CONSUME(3);

    /* --- Offset Huffman table (method 3 only) --- */
    if (s->decrunch_method == 3) {
        for (int i = 0; i < 8; i++) {
            s->offset_len[i] = (uint8_t)(control & 7);
            CONSUME(3);
        }
        if (!make_decode_table(8, 7, s->offset_len, s->offset_table))
            return false;
    }

    /* --- Decrunch length (3 bytes, big-endian within the bit stream) --- */
    s->decrunch_length  = (int32_t)((control & 0xFF) << 16); CONSUME(8);
    s->decrunch_length |= (int32_t)((control & 0xFF) << 8);  CONSUME(8);
    s->decrunch_length |= (int32_t) (control & 0xFF);         CONSUME(8);

    /* --- Literal Huffman table (methods 2 and 3) --- */
    if (s->decrunch_method == 1) {
        /* method 1 = stored, no literal table needed */
        s->control = control;
        s->shift   = shift;
        return true;
    }

    uint32_t pos        = 0;
    int      fix        = 1;
    uint32_t max_symbol = 256;

    do {
        /* Read 20 x 4-bit code lengths for the secondary huffman table. */
        for (int i = 0; i < 20; i++) {
            s->huffman20_len[i] = (uint8_t)(control & 0xF);
            control >>= 4;
            shift   -= 4;
            REFILL();
        }

        if (!make_decode_table(20, 6, s->huffman20_len, s->huffman20_table))
            return false;

        do {
            uint32_t sym;

            /* Decode one symbol from the secondary table. */
            sym = s->huffman20_table[control & 63];
            if (sym >= 20) {
                do {
                    sym = s->huffman20_table[((control >> 6) & 1) + (sym << 1)];
                    if (!shift--) { shift += 16; control += (uint32_t)(*s->src++) << 24; control += (uint32_t)(*s->src++) << 16; }
                    control >>= 1;
                } while (sym >= 20);
                CONSUME(6);
            } else {
                CONSUME(s->huffman20_len[sym]);
            }

            uint32_t temp, count;
            switch (sym) {
            case 17: /* run of zeros, short */
            case 18: /* run of zeros, long  */
                temp  = (sym == 17) ? 4 : (uint32_t)(6 - fix);
                count = (sym == 17) ? 3 : 19;
                count += (control & table_three[temp]) + (uint32_t)fix;
                CONSUME(temp);
                while (pos < max_symbol && count--)
                    s->literal_len[pos++] = 0;
                break;

            case 19: /* run of a repeated length delta */
                count = (control & 1) + 3 + (uint32_t)fix;
                if (!shift--) { shift += 16; control += (uint32_t)(*s->src++) << 24; control += (uint32_t)(*s->src++) << 16; }
                control >>= 1;

                sym = s->huffman20_table[control & 63];
                if (sym >= 20) {
                    do {
                        sym = s->huffman20_table[((control >> 6) & 1) + (sym << 1)];
                        if (!shift--) { shift += 16; control += (uint32_t)(*s->src++) << 24; control += (uint32_t)(*s->src++) << 16; }
                        control >>= 1;
                    } while (sym >= 20);
                    CONSUME(6);
                } else {
                    CONSUME(s->huffman20_len[sym]);
                }

                sym = table_four[s->literal_len[pos] + 17 - sym];
                while (pos < max_symbol && count--)
                    s->literal_len[pos++] = (uint8_t)sym;
                break;

            default: /* single length delta */
                sym = table_four[s->literal_len[pos] + 17 - sym];
                s->literal_len[pos++] = (uint8_t)sym;
                break;
            }
        } while (pos < max_symbol);

        fix--;
        max_symbol += 512;
    } while (max_symbol == 768);

    s->control = control;
    s->shift   = shift;

#undef CONSUME
#undef REFILL

    return make_decode_table(768, 12, s->literal_len, s->literal_table);
}

/* -------------------------------------------------------------------------
 * Core decompression loop
 * ---------------------------------------------------------------------- */

/*
 * Decompress data from s->src into s->dst until dst reaches dst_end
 * or src reaches src_end. Both buffers have enough overrun that we
 * don't need per-byte range checks inside the hot loop.
 */
static void decrunch(decrunch_state_t *s)
{
    uint32_t control = s->control;
    int      shift   = s->shift;

#define REFILL() do {                                           \
    if (shift < 0) {                                           \
        shift   += 16;                                         \
        control += (uint32_t)(*s->src++) << (8 + shift);      \
        control += (uint32_t)(*s->src++) << shift;             \
    }                                                          \
} while (0)

#define CONSUME(n) do { control >>= (n); shift -= (n); REFILL(); } while (0)

    do {
        uint32_t sym = s->literal_table[control & 0xFFF];

        if (sym >= 768) {
            /* Code longer than 12 bits: follow the chain. */
            CONSUME(12);
            do {
                sym = s->literal_table[(control & 1) + (sym << 1)];
                if (!shift--) {
                    shift   += 16;
                    control += (uint32_t)(*s->src++) << 24;
                    control += (uint32_t)(*s->src++) << 16;
                }
                control >>= 1;
            } while (sym >= 768);
        } else {
            CONSUME(s->literal_len[sym]);
        }

        if (sym < 256) {
            /* Literal byte */
            *s->dst++ = (uint8_t)sym;
        } else {
            /* Back-reference: decode offset and match length. */
            sym -= 256;

            uint32_t temp  = sym & 31;
            uint32_t count = table_two[temp];
            temp = table_one[temp];

            if (temp >= 3 && s->decrunch_method == 3) {
                temp -= 3;
                count += (control & table_three[temp]) << 3;
                CONSUME(temp);
                count += (temp = s->offset_table[control & 127]);
                temp   = s->offset_len[temp];
            } else {
                count += control & table_three[temp];
                if (!count) count = s->last_offset;
            }
            CONSUME(temp);
            s->last_offset = count;

            /* Match length */
            temp   = (sym >> 5) & 15;
            uint32_t match_len = table_two[temp] + 3;
            temp   = table_one[temp];
            match_len += (control & table_three[temp]);
            CONSUME(temp);

            /* Copy from back-reference, handling wrap-around in the ring. */
            uint8_t *str = (s->decrunch_buffer + s->last_offset < s->dst)
                           ? s->dst - s->last_offset
                           : s->dst + DECRUNCH_WINDOW - s->last_offset;
            do {
                *s->dst++ = *str++;
            } while (--match_len);
        }
    } while (s->dst < s->dst_end && s->src < s->src_end);

#undef CONSUME
#undef REFILL

    s->control = control;
    s->shift   = shift;
}

/* -------------------------------------------------------------------------
 * File I/O helpers
 * ---------------------------------------------------------------------- */

/*
 * Open 'filename' for writing.  Create intermediate directories as
 * needed (ignoring mkdir failures — the subsequent fopen will report
 * any real error).
 */
static FILE *open_output(char *filename)
{
    FILE *f = fopen(filename, "wb");
    if (f) return f;

    /* Try to create parent directories one component at a time. */
    for (size_t i = 0; filename[i]; i++) {
        if (filename[i] == '/') {
            filename[i] = '\0';
            mkdir(filename, 0755);
            filename[i] = '/';
        }
    }

    f = fopen(filename, "wb");
    if (!f) perror("fopen");
    return f;
}

/* -------------------------------------------------------------------------
 * Extraction back-ends
 * ---------------------------------------------------------------------- */

/*
 * extract_normal: decompress a PACK_NORMAL block for every file in list.
 *
 * The compressed stream may span multiple files (merged blocks), so
 * the decrunch state is kept across the list iteration.
 */
static bool extract_normal(FILE *in_file, filename_node_t *list,
                            uint32_t pack_size)
{
    decrunch_state_t *s = calloc(1, sizeof(*s));
    if (!s) { fprintf(stderr, "Out of memory\n"); return false; }

    s->control        = 0;
    s->shift          = -16;
    s->last_offset    = 1;
    s->unpack_size    = 0;
    s->decrunch_length= 0;
    s->pack_size      = pack_size;

    s->src_end = (s->src = s->read_buffer + READ_BUFFER_SIZE) - 1024;
    uint8_t *pos = s->dst_end = s->dst = s->decrunch_buffer + 258 + DECRUNCH_WINDOW;

    bool failed = false;

    for (filename_node_t *node = list; !failed && node; node = node->next) {
        printf("Extracting \"%s\"...", node->filename);
        fflush(stdout);

        FILE *out = open_output(node->filename);
        s->crc_sum    = 0;
        s->unpack_size= node->length;

        while (s->unpack_size > 0) {
            if (pos == s->dst) {
                /* Refill source buffer if needed. */
                if (s->src >= s->src_end) {
                    uint8_t *tmp = s->read_buffer;
                    uint32_t carry = (uint32_t)(tmp - s->src) + READ_BUFFER_SIZE;
                    if (carry) {
                        uint8_t *src = s->src;
                        while (carry--) *tmp++ = *src++;
                    }
                    s->src = s->read_buffer;
                    uint32_t want = (uint32_t)(s->src - tmp) + READ_BUFFER_SIZE;
                    if (s->pack_size < want) want = s->pack_size;

                    if (fread(tmp, 1, want, in_file) != want) {
                        printf("\n");
                        if (ferror(in_file)) perror("fread(data)");
                        else fprintf(stderr, "EOF: data\n");
                        failed = true;
                        break;
                    }
                    s->pack_size -= want;
                    tmp += want;
                    if (s->src >= tmp) break;
                }

                if (s->decrunch_length <= 0) {
                    if (!read_literal_table(s)) {
                        fprintf(stderr, "Corrupt Huffman table\n");
                        failed = true;
                        break;
                    }
                }

                /* Wrap the ring buffer when it gets full. */
                if (s->dst >= s->decrunch_buffer + 258 + DECRUNCH_WINDOW) {
                    uint32_t overrun = (uint32_t)(s->dst - s->decrunch_buffer - DECRUNCH_WINDOW);
                    if (overrun) {
                        uint8_t *wrap_dst = s->decrunch_buffer;
                        uint8_t *wrap_src = wrap_dst + DECRUNCH_WINDOW;
                        s->dst = wrap_dst;
                        while (overrun--) *s->dst++ = *wrap_src++;
                    }
                    pos = s->dst;
                }

                s->dst_end = s->dst + (uint32_t)s->decrunch_length;
                if (s->dst_end > s->decrunch_buffer + 258 + DECRUNCH_WINDOW)
                    s->dst_end = s->decrunch_buffer + 258 + DECRUNCH_WINDOW;

                uint8_t *before = s->dst;
                decrunch(s);
                s->decrunch_length -= (int32_t)(s->dst - before);
            }

            uint32_t count = (uint32_t)(s->dst - pos);
            if (count > s->unpack_size) count = s->unpack_size;

            crc_update(&s->crc_sum, pos, count);

            if (out && fwrite(pos, 1, count, out) != count) {
                perror("fwrite");
                fclose(out);
                out = NULL;
            }

            s->unpack_size -= count;
            pos            += count;
        }

        if (out) {
            fclose(out);
            if (!failed)
                printf(" crc %s\n", (node->crc == s->crc_sum) ? "good" : "bad");
        }
    }

    free(s);
    return !failed;
}

/* -------------------------------------------------------------------------
 * extract_store: copy raw (uncompressed) data to output file(s).
 * ---------------------------------------------------------------------- */
static bool extract_store(FILE *in_file, filename_node_t *list,
                           uint32_t pack_size)
{
    uint8_t  buf[READ_BUFFER_SIZE];
    bool     failed = false;

    for (filename_node_t *node = list; !failed && node; node = node->next) {
        printf("Storing \"%s\"...", node->filename);
        fflush(stdout);

        FILE    *out        = open_output(node->filename);
        uint32_t crc_sum    = 0;
        uint32_t remaining  = node->length;
        if (remaining > pack_size) remaining = pack_size;

        while (remaining > 0) {
            uint32_t want = (remaining > READ_BUFFER_SIZE)
                            ? READ_BUFFER_SIZE : remaining;

            if (fread(buf, 1, want, in_file) != want) {
                printf("\n");
                if (ferror(in_file)) perror("fread(data)");
                else fprintf(stderr, "EOF: data\n");
                failed = true;
                break;
            }
            pack_size -= want;
            crc_update(&crc_sum, buf, want);

            if (out && fwrite(buf, 1, want, out) != want) {
                perror("fwrite");
                fclose(out);
                out = NULL;
            }
            remaining -= want;
        }

        if (out) {
            fclose(out);
            if (!failed)
                printf(" crc %s\n", (node->crc == crc_sum) ? "good" : "bad");
        }
    }
    return !failed;
}

/* -------------------------------------------------------------------------
 * extract_unknown: report unsupported compression methods.
 * ---------------------------------------------------------------------- */
static bool extract_unknown(filename_node_t *list)
{
    for (filename_node_t *node = list; node; node = node->next)
        printf("Unknown pack mode: \"%s\"\n", node->filename);
    return true; /* not a fatal error; keep going */
}

/* -------------------------------------------------------------------------
 * Linked-list helpers
 * ---------------------------------------------------------------------- */
static void free_filename_list(filename_node_t *list)
{
    while (list) {
        filename_node_t *next = list->next;
        free(list);
        list = next;
    }
}

/* -------------------------------------------------------------------------
 * Archive-level parsing: extract
 * ---------------------------------------------------------------------- */

/*
 * Read archive headers, build a filename list, and dispatch to the
 * appropriate extraction routine.
 *
 * Returns 0 on success, 1 on error.
 */
static int extract_archive(FILE *in_file)
{
    uint8_t          archive_header[ARCHIVE_HEADER_SIZE];
    uint8_t          header_filename[MAX_FILENAME_LEN + 1];
    uint8_t          header_comment[MAX_COMMENT_LEN  + 1];

    filename_node_t *filename_list = NULL;
    filename_node_t **list_tail    = &filename_list;

    int result = 1; /* assume error */

    while (true) {
        size_t actual = fread(archive_header, 1, ARCHIVE_HEADER_SIZE, in_file);

        if (ferror(in_file)) { perror("fread(archive_header)"); break; }
        if (actual == 0)     { result = 0; break; } /* normal EOF */
        if (actual != ARCHIVE_HEADER_SIZE) {
            fprintf(stderr, "EOF: archive_header\n"); break;
        }

        /* Verify header CRC. The 4-byte CRC field must be zeroed first. */
        uint32_t expected_crc = ((uint32_t)archive_header[29] << 24)
                              | ((uint32_t)archive_header[28] << 16)
                              | ((uint32_t)archive_header[27] <<  8)
                              |  (uint32_t)archive_header[26];
        archive_header[29] = archive_header[28] =
        archive_header[27] = archive_header[26] = 0;

        uint32_t hdr_crc = 0;
        crc_update(&hdr_crc, archive_header, ARCHIVE_HEADER_SIZE);

        /* Filename */
        uint32_t fname_len = archive_header[30];
        actual = fread(header_filename, 1, fname_len, in_file);
        if (ferror(in_file)) { perror("fread(header_filename)"); break; }
        if (actual != fname_len) { fprintf(stderr, "EOF: header_filename\n"); break; }
        header_filename[fname_len] = '\0';
        crc_update(&hdr_crc, header_filename, fname_len);

        /* Comment */
        uint32_t cmt_len = archive_header[14];
        actual = fread(header_comment, 1, cmt_len, in_file);
        if (ferror(in_file)) { perror("fread(header_comment)"); break; }
        if (actual != cmt_len) { fprintf(stderr, "EOF: header_comment\n"); break; }
        header_comment[cmt_len] = '\0';
        crc_update(&hdr_crc, header_comment, cmt_len);

        if (hdr_crc != expected_crc) {
            fprintf(stderr, "CRC: archive_header\n"); break;
        }

        uint32_t unpack_size = ((uint32_t)archive_header[5] << 24)
                             | ((uint32_t)archive_header[4] << 16)
                             | ((uint32_t)archive_header[3] <<  8)
                             |  (uint32_t)archive_header[2];
        uint32_t pack_size   = ((uint32_t)archive_header[9] << 24)
                             | ((uint32_t)archive_header[8] << 16)
                             | ((uint32_t)archive_header[7] <<  8)
                             |  (uint32_t)archive_header[6];
        uint8_t  pack_mode   =  archive_header[11];
        uint32_t data_crc    = ((uint32_t)archive_header[25] << 24)
                             | ((uint32_t)archive_header[24] << 16)
                             | ((uint32_t)archive_header[23] <<  8)
                             |  (uint32_t)archive_header[22];

        /* Append a node for this file. */
        filename_node_t *node = malloc(sizeof(*node));
        if (!node) { fprintf(stderr, "malloc(filename_node)\n"); break; }

        node->next   = NULL;
        node->length = unpack_size;
        node->crc    = data_crc;
        strncpy(node->filename, (char *)header_filename, MAX_FILENAME_LEN);
        node->filename[MAX_FILENAME_LEN] = '\0';

        *list_tail = node;
        list_tail  = &node->next;

        if (!pack_size) continue; /* merged empty entry */

        bool ok;
        switch (pack_mode) {
        case PACK_STORE:  ok = extract_store  (in_file, filename_list, pack_size); break;
        case PACK_NORMAL: ok = extract_normal (in_file, filename_list, pack_size); break;
        default:          ok = extract_unknown(filename_list);                      break;
        }

        free_filename_list(filename_list);
        filename_list = NULL;
        list_tail     = &filename_list;

        if (!ok) break;

        if (fseek(in_file, (long)pack_size, SEEK_CUR) != 0) {
            perror("fseek(data)"); break;
        }

        result = 0; /* tentatively OK; loop again */
    }

    free_filename_list(filename_list);
    return result;
}

/* -------------------------------------------------------------------------
 * Date/time formatting — RFC 3339
 *
 * RFC 3339 (profil strict d'ISO 8601) impose le format :
 *   YYYY-MM-DD HH:MM:SS +HH:MM   (heure locale avec offset explicite)
 *
 * Les dates LZX sont stockées sans information de fuseau horaire.
 * On les interprète comme heure locale.  L'offset UTC est calculé
 * pour la date exacte du fichier, ce qui garantit que l'heure d'été
 * (DST) est correctement prise en compte : un fichier daté du mois
 * de juillet affichera +02:00 en Europe centrale, et un fichier de
 * janvier affichera +01:00.
 *
 * buf doit avoir au moins 28 octets ( "YYYY-MM-DD HH:MM:SS +HH:MM\0" ).
 * ---------------------------------------------------------------------- */

/*
 * Formate year/month/day/hour/minute/second en une chaîne RFC 3339
 * avec l'offset UTC correct pour cette date précise.
 * month est 0-based (comme dans le format LZX).
 *
 * Exemple : "1998-07-14 14:37:00 +02:00"  (heure d'été)
 *           "1998-02-22 14:37:00 +01:00"  (heure d'hiver)
 */
static void format_rfc3339(char *buf, size_t bufsz,
                            uint32_t year,   uint32_t month,
                            uint32_t day,    uint32_t hour,
                            uint32_t minute, uint32_t second)
{
    /*
     * Construire une struct tm à partir des champs LZX, puis appeler
     * mktime() avec tm_isdst = -1 : la libc détermine automatiquement
     * si la date tombe en heure d'été ou d'hiver pour la timezone
     * locale, et renseigne tm_gmtoff avec l'offset exact en secondes.
     */
    struct tm t = {
        .tm_year  = (int)year  - 1900,
        .tm_mon   = (int)month,        /* 0-based, identique à LZX */
        .tm_mday  = (int)day,
        .tm_hour  = (int)hour,
        .tm_min   = (int)minute,
        .tm_sec   = (int)second,
        .tm_isdst = -1,                /* laisser mktime() décider */
    };
    mktime(&t);   /* normalise t et renseigne tm_gmtoff + tm_isdst */

    long off_sec = t.tm_gmtoff;       /* offset en secondes, signé */
    char sign    = (off_sec >= 0) ? '+' : '-';
    long off_abs = (off_sec >= 0) ? off_sec : -off_sec;
    int  off_h   = (int)(off_abs / 3600) % 100;
    int  off_m   = (int)((off_abs % 3600) / 60);

    /* month + 1 : passage de 0-based (LZX) à 1-based (affichage). */
    snprintf(buf, bufsz, "%04u-%02u-%02u %02u:%02u:%02u %c%02d:%02d",
             year, month + 1, day,
             hour, minute, second,
             sign, off_h, off_m);
}

/* -------------------------------------------------------------------------
 * Archive-level parsing: view / list
 * ---------------------------------------------------------------------- */

static int view_archive(FILE *in_file)
{
    uint8_t  archive_header[ARCHIVE_HEADER_SIZE];
    uint8_t  header_filename[MAX_FILENAME_LEN + 1];
    uint8_t  header_comment[MAX_COMMENT_LEN  + 1];

    uint32_t total_pack   = 0;
    uint32_t total_unpack = 0;
    uint32_t total_files  = 0;
    uint32_t merge_size   = 0;
    int      result       = 1;

    printf("Unpacked   Packed  Timestamp (RFC 3339)         Attrib   Name\n");
    printf("-------- -------- ---------------------------  -------- ----\n");

    while (true) {
        size_t actual = fread(archive_header, 1, ARCHIVE_HEADER_SIZE, in_file);

        if (ferror(in_file)) { perror("fread(archive_header)"); break; }
        if (actual == 0) {
            /* Normal EOF: print summary. */
            printf("-------- -------- ---------------------------  -------- ----\n");
            printf("%8u %8u  %u file%s\n",
                   total_unpack, total_pack,
                   total_files, total_files == 1 ? "" : "s");
            result = 0;
            break;
        }
        if (actual != ARCHIVE_HEADER_SIZE) {
            fprintf(stderr, "EOF: archive_header\n"); break;
        }

        uint32_t expected_crc = ((uint32_t)archive_header[29] << 24)
                              | ((uint32_t)archive_header[28] << 16)
                              | ((uint32_t)archive_header[27] <<  8)
                              |  (uint32_t)archive_header[26];
        archive_header[29] = archive_header[28] =
        archive_header[27] = archive_header[26] = 0;

        uint32_t hdr_crc = 0;
        crc_update(&hdr_crc, archive_header, ARCHIVE_HEADER_SIZE);

        uint32_t fname_len = archive_header[30];
        actual = fread(header_filename, 1, fname_len, in_file);
        if (ferror(in_file)) { perror("fread(header_filename)"); break; }
        if (actual != fname_len) { fprintf(stderr, "EOF: header_filename\n"); break; }
        header_filename[fname_len] = '\0';
        crc_update(&hdr_crc, header_filename, fname_len);

        uint32_t cmt_len = archive_header[14];
        actual = fread(header_comment, 1, cmt_len, in_file);
        if (ferror(in_file)) { perror("fread(header_comment)"); break; }
        if (actual != cmt_len) { fprintf(stderr, "EOF: header_comment\n"); break; }
        header_comment[cmt_len] = '\0';
        crc_update(&hdr_crc, header_comment, cmt_len);

        if (hdr_crc != expected_crc) {
            fprintf(stderr, "CRC: archive_header\n"); break;
        }

        uint8_t  attr      =  archive_header[0];
        uint32_t unpack_sz = ((uint32_t)archive_header[5] << 24)
                           | ((uint32_t)archive_header[4] << 16)
                           | ((uint32_t)archive_header[3] <<  8)
                           |  (uint32_t)archive_header[2];
        uint32_t pack_sz   = ((uint32_t)archive_header[9] << 24)
                           | ((uint32_t)archive_header[8] << 16)
                           | ((uint32_t)archive_header[7] <<  8)
                           |  (uint32_t)archive_header[6];
        uint32_t date_raw  = ((uint32_t)archive_header[18] << 24)
                           | ((uint32_t)archive_header[19] << 16)
                           | ((uint32_t)archive_header[20] <<  8)
                           |  (uint32_t)archive_header[21];

        uint32_t year   = ((date_raw >> 17) & 63) + 1970;
        uint32_t month  =  (date_raw >> 23) & 15;   /* 0-based */
        uint32_t day    =  (date_raw >> 27) & 31;
        uint32_t hour   =  (date_raw >> 12) & 31;
        uint32_t minute =  (date_raw >>  6) & 63;
        uint32_t second =   date_raw        & 63;

        char ts[28];
        format_rfc3339(ts, sizeof(ts), year, month, day, hour, minute, second);

        total_pack   += pack_sz;
        total_unpack += unpack_sz;
        total_files++;
        merge_size   += unpack_sz;

        printf("%8u ", unpack_sz);
        if (archive_header[12] & HDR_FLAG_MERGED)
            printf("     n/a ");
        else
            printf("%8u ", pack_sz);

        printf("%s  ", ts);
        printf("%c%c%c%c%c%c%c%c  ",
               (attr & HDR_PROT_HOLD)    ? 'h' : '-',
               (attr & HDR_PROT_SCRIPT)  ? 's' : '-',
               (attr & HDR_PROT_PURE)    ? 'p' : '-',
               (attr & HDR_PROT_ARCHIVE) ? 'a' : '-',
               (attr & HDR_PROT_READ)    ? 'r' : '-',
               (attr & HDR_PROT_WRITE)   ? 'w' : '-',
               (attr & HDR_PROT_EXECUTE) ? 'e' : '-',
               (attr & HDR_PROT_DELETE)  ? 'd' : '-');
        printf("\"%s\"\n", header_filename);

        if (header_comment[0])
            printf(": \"%s\"\n", header_comment);
        if ((archive_header[12] & HDR_FLAG_MERGED) && pack_sz)
            printf("%8u %8u  Merged\n", merge_size, pack_sz);

        if (pack_sz) {
            merge_size = 0;
            if (fseek(in_file, (long)pack_sz, SEEK_CUR) != 0) {
                perror("fseek"); break;
            }
        }
    }

    return result;
}

/* -------------------------------------------------------------------------
 * Top-level archive dispatcher
 * ---------------------------------------------------------------------- */

typedef enum { MODE_EXTRACT = 1, MODE_VIEW = 2 } run_mode_t;

static int process_archive(const char *filename, run_mode_t mode)
{
    FILE    *in_file;
    uint8_t  info_header[INFO_HEADER_SIZE];

    if (!filename) {
        in_file = stdin;
    } else {
        in_file = fopen(filename, "rb");
        if (!in_file) { perror("fopen(archive)"); return 1; }
    }

    int result = 1;

    if (fread(info_header, 1, INFO_HEADER_SIZE, in_file) != INFO_HEADER_SIZE) {
        if (ferror(in_file)) perror("fread(info_header)");
        else fprintf(stderr, "EOF: info_header\n");
    } else if (info_header[0] != LZX_MAGIC_0 ||
               info_header[1] != LZX_MAGIC_1 ||
               info_header[2] != LZX_MAGIC_2) {
        fprintf(stderr, "Not an LZX archive: %s\n",
                filename ? filename : "<stdin>");
    } else {
        switch (mode) {
        case MODE_EXTRACT: result = extract_archive(in_file); break;
        case MODE_VIEW:    result = view_archive(in_file);    break;
        }
    }

    if (filename) fclose(in_file);
    return result;
}

/* -------------------------------------------------------------------------
 * Entry point
 * ---------------------------------------------------------------------- */

static void usage(const char *prog)
{
    fprintf(stderr,
            "Usage: %s [-v|-x] [-c] [archive ...]\n"
            "  -c  read from stdin\n"
            "  -v  list archive contents\n"
            "  -x  extract (default)\n",
            prog ? prog : "unlzx");
}

int main(int argc, char **argv)
{
    run_mode_t mode           = MODE_EXTRACT;
    bool       read_from_stdin = false;
    int        opt;

    while ((opt = getopt(argc, argv, "vxc")) != -1) {
        switch (opt) {
        case 'v': mode = MODE_VIEW;    break;
        case 'x': mode = MODE_EXTRACT; break;
        case 'c': read_from_stdin = true; break;
        default:
            usage(argv[0]);
            return 2;
        }
    }

    if (!read_from_stdin && optind >= argc) {
        usage(argv[0]);
        return 2;
    }

    int result = 0;

    if (read_from_stdin) {
        printf("\nReading from stdin...\n\n");
        result = process_archive(NULL, mode);
    }

    if (argc - optind > 1) {
        /* Multiple archives: report errors but don't propagate one failure
         * as the overall result (consistent with the original behaviour). */
        for (; optind < argc; optind++) {
            printf("\nArchive \"%s\"...\n\n", argv[optind]);
            process_archive(argv[optind], mode);
        }
        result = 0;
    } else if (optind < argc) {
        result = process_archive(argv[optind], mode);
    }

    return result;
}
