diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2cd2cbc..87ceb68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -145,7 +145,23 @@ add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h a
ha/hsc.h
ha/internal.h
ha/swdict.c
- ha/swdict.h)
+ ha/swdict.h
+ lha/bitio.c
+ lha/bitio.h
+ lha/lzss.c
+ lha/lzss.h
+ lha/huffman.c
+ lha/huffman.h
+ lha/larc.c
+ lha/larc.h
+ lha/lh1.c
+ lha/lh1.h
+ lha/lh_static.c
+ lha/lh_static.h
+ lha/lh_old.c
+ lha/lh_old.h
+ lha/pmarc1.c
+ lha/pmarc1.h)
include(3rdparty/bzip2.cmake)
include(3rdparty/flac.cmake)
diff --git a/lha/bitio.c b/lha/bitio.c
new file mode 100644
index 0000000..22cfe7c
--- /dev/null
+++ b/lha/bitio.c
@@ -0,0 +1,98 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "bitio.h"
+
+#include
+
+void lha_bitio_init(lha_bitio *self, const uint8_t *buffer, size_t length)
+{
+ self->buffer = buffer;
+ self->length = length;
+ self->pos = 0;
+ self->bits = 0;
+ self->numbits = 0;
+ self->eof = false;
+}
+
+static void lha_bitio_fill(lha_bitio *self)
+{
+ while(self->numbits <= 24 && self->pos < self->length)
+ {
+ self->bits |= (uint32_t)self->buffer[self->pos++] << (24 - self->numbits);
+ self->numbits += 8;
+ }
+
+ if(self->pos >= self->length && self->numbits == 0) self->eof = true;
+}
+
+bool lha_bitio_at_eof(lha_bitio *self) { return self->eof && self->numbits == 0; }
+
+unsigned int lha_bitio_next_bit(lha_bitio *self)
+{
+ if(self->numbits == 0) lha_bitio_fill(self);
+
+ if(self->numbits == 0) return 0;
+
+ unsigned int bit = (self->bits >> 31) & 1;
+ self->bits <<= 1;
+ self->numbits--;
+ return bit;
+}
+
+unsigned int lha_bitio_next_bits(lha_bitio *self, int numbits)
+{
+ if(numbits == 0) return 0;
+
+ if((int)self->numbits < numbits) lha_bitio_fill(self);
+
+ unsigned int result = self->bits >> (32 - numbits);
+ self->bits <<= numbits;
+ self->numbits -= numbits;
+ return result;
+}
+
+unsigned int lha_bitio_peek_bits(lha_bitio *self, int numbits)
+{
+ if(numbits == 0) return 0;
+
+ if((int)self->numbits < numbits) lha_bitio_fill(self);
+
+ return self->bits >> (32 - numbits);
+}
+
+void lha_bitio_skip_bits(lha_bitio *self, int numbits)
+{
+ self->bits <<= numbits;
+ self->numbits -= numbits;
+}
+
+int lha_bitio_next_byte(lha_bitio *self)
+{
+ if(self->pos >= self->length && self->numbits < 8) return -1;
+
+ /* If on byte boundary, read directly */
+ if(self->numbits == 0)
+ {
+ if(self->pos < self->length) return self->buffer[self->pos++];
+
+ return -1;
+ }
+
+ return (int)lha_bitio_next_bits(self, 8);
+}
diff --git a/lha/bitio.h b/lha/bitio.h
new file mode 100644
index 0000000..86c6ad6
--- /dev/null
+++ b/lha/bitio.h
@@ -0,0 +1,44 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_BITIO_H_
+#define AARU_COMPRESSION_NATIVE__LHA_BITIO_H_
+
+#include
+#include
+#include
+
+typedef struct lha_bitio
+{
+ const uint8_t *buffer;
+ size_t length;
+ size_t pos;
+ uint32_t bits;
+ unsigned int numbits;
+ bool eof;
+} lha_bitio;
+
+void lha_bitio_init(lha_bitio *self, const uint8_t *buffer, size_t length);
+bool lha_bitio_at_eof(lha_bitio *self);
+unsigned int lha_bitio_next_bit(lha_bitio *self);
+unsigned int lha_bitio_next_bits(lha_bitio *self, int numbits);
+unsigned int lha_bitio_peek_bits(lha_bitio *self, int numbits);
+void lha_bitio_skip_bits(lha_bitio *self, int numbits);
+int lha_bitio_next_byte(lha_bitio *self);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_BITIO_H_ */
diff --git a/lha/huffman.c b/lha/huffman.c
new file mode 100644
index 0000000..8d05238
--- /dev/null
+++ b/lha/huffman.c
@@ -0,0 +1,259 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "huffman.h"
+
+#include
+#include
+#include
+
+/* --- Internal helpers --- */
+
+static inline int left_branch(lha_prefix_code *c, int node) { return c->tree[node].branches[0]; }
+
+static inline int right_branch(lha_prefix_code *c, int node) { return c->tree[node].branches[1]; }
+
+static inline void set_branch(lha_prefix_code *c, int node, int bit, int next)
+{
+ c->tree[node].branches[bit] = next;
+}
+
+static inline bool is_leaf(lha_prefix_code *c, int node)
+{
+ return c->tree[node].branches[0] == c->tree[node].branches[1];
+}
+
+static inline int leaf_value(lha_prefix_code *c, int node) { return left_branch(c, node); }
+
+static inline void set_leaf(lha_prefix_code *c, int node, int value)
+{
+ c->tree[node].branches[0] = value;
+ c->tree[node].branches[1] = value;
+}
+
+static inline void set_empty(lha_prefix_code *c, int node)
+{
+ c->tree[node].branches[0] = -1;
+ c->tree[node].branches[1] = -2;
+}
+
+static inline bool is_empty(lha_prefix_code *c, int node)
+{
+ return c->tree[node].branches[0] == -1 && c->tree[node].branches[1] == -2;
+}
+
+static inline bool is_open(lha_prefix_code *c, int node, int bit) { return c->tree[node].branches[bit] < 0; }
+
+static int new_node(lha_prefix_code *c)
+{
+ lha_tree_node *tmp = (lha_tree_node *)realloc(c->tree, (size_t)(c->numentries + 1) * sizeof(lha_tree_node));
+
+ if(!tmp) return -1;
+
+ c->tree = tmp;
+ set_empty(c, c->numentries);
+ return c->numentries++;
+}
+
+/* --- Build lookup table --- */
+
+static void make_table(lha_prefix_code *code, int node, lha_table_entry *table, int depth, int maxdepth)
+{
+ int currtablesize = 1 << (maxdepth - depth);
+
+ if(node < 0)
+ {
+ int i;
+ for(i = 0; i < currtablesize; i++) table[i].length = (uint32_t)-1;
+ }
+ else if(is_leaf(code, node))
+ {
+ int i;
+ for(i = 0; i < currtablesize; i++)
+ {
+ table[i].length = (uint32_t)depth;
+ table[i].value = leaf_value(code, node);
+ }
+ }
+ else
+ {
+ if(depth == maxdepth)
+ {
+ table[0].length = (uint32_t)(maxdepth + 1);
+ table[0].value = node;
+ }
+ else
+ {
+ make_table(code, left_branch(code, node), table, depth + 1, maxdepth);
+ make_table(code, right_branch(code, node), table + currtablesize / 2, depth + 1, maxdepth);
+ }
+ }
+}
+
+static void build_table(lha_prefix_code *code)
+{
+ if(code->table) return;
+
+ if(code->maxlength < code->minlength)
+ code->tablesize = LHA_HUFFMAN_TABLE_SIZE;
+ else if(code->maxlength >= LHA_HUFFMAN_TABLE_SIZE)
+ code->tablesize = LHA_HUFFMAN_TABLE_SIZE;
+ else
+ code->tablesize = code->maxlength;
+
+ code->table = (lha_table_entry *)malloc(sizeof(lha_table_entry) * (size_t)(1 << code->tablesize));
+
+ if(code->table) make_table(code, 0, code->table, 0, code->tablesize);
+}
+
+/* --- Public API --- */
+
+lha_prefix_code *lha_prefix_code_new(void)
+{
+ lha_prefix_code *code = (lha_prefix_code *)calloc(1, sizeof(lha_prefix_code));
+
+ if(!code) return NULL;
+
+ code->tree = (lha_tree_node *)malloc(sizeof(lha_tree_node));
+
+ if(!code->tree)
+ {
+ free(code);
+ return NULL;
+ }
+
+ set_empty(code, 0);
+ code->numentries = 1;
+ code->minlength = INT_MAX;
+ code->maxlength = INT_MIN;
+ code->table = NULL;
+ code->tablesize = 0;
+
+ return code;
+}
+
+lha_prefix_code *lha_prefix_code_from_lengths(const int *lengths, int num_symbols, int max_length,
+ bool shortest_is_zeros)
+{
+ lha_prefix_code *code = lha_prefix_code_new();
+
+ if(!code) return NULL;
+
+ int codeval = 0;
+ int symbolsleft = num_symbols;
+ int length, i;
+
+ for(length = 1; length <= max_length; length++)
+ {
+ for(i = 0; i < num_symbols; i++)
+ {
+ if(lengths[i] != length) continue;
+
+ if(shortest_is_zeros)
+ lha_prefix_code_add(code, i, (uint32_t)codeval, length);
+ else
+ lha_prefix_code_add(code, i, (uint32_t)~codeval, length);
+
+ codeval++;
+
+ if(--symbolsleft == 0) return code;
+ }
+
+ codeval <<= 1;
+ }
+
+ return code;
+}
+
+void lha_prefix_code_free(lha_prefix_code *code)
+{
+ if(!code) return;
+
+ free(code->tree);
+ free(code->table);
+ free(code);
+}
+
+void lha_prefix_code_add(lha_prefix_code *code, int value, uint32_t codebits, int length)
+{
+ int bitpos, lastnode;
+
+ /* Invalidate lookup table */
+ free(code->table);
+ code->table = NULL;
+
+ if(length > code->maxlength) code->maxlength = length;
+ if(length < code->minlength) code->minlength = length;
+
+ lastnode = 0;
+
+ for(bitpos = length - 1; bitpos >= 0; bitpos--)
+ {
+ int bit = (codebits >> bitpos) & 1;
+
+ if(is_leaf(code, lastnode)) return; /* prefix collision */
+
+ if(is_open(code, lastnode, bit)) set_branch(code, lastnode, bit, new_node(code));
+
+ lastnode = code->tree[lastnode].branches[bit];
+
+ if(lastnode < 0) return; /* allocation failure */
+ }
+
+ if(!is_empty(code, lastnode)) return; /* prefix collision */
+
+ set_leaf(code, lastnode, value);
+}
+
+int lha_prefix_code_decode(lha_bitio *bitio, lha_prefix_code *code)
+{
+ int bits, length, value, node;
+
+ if(!code) return -1;
+
+ build_table(code);
+
+ if(!code->table) return -1;
+
+ bits = (int)lha_bitio_peek_bits(bitio, code->tablesize);
+ length = (int)code->table[bits].length;
+ value = code->table[bits].value;
+
+ if(length == (int)(uint32_t)-1) return -1; /* invalid code */
+
+ if(length <= code->tablesize)
+ {
+ lha_bitio_skip_bits(bitio, length);
+ return value;
+ }
+
+ lha_bitio_skip_bits(bitio, code->tablesize);
+
+ node = value;
+
+ while(!is_leaf(code, node))
+ {
+ int bit = (int)lha_bitio_next_bit(bitio);
+
+ if(is_open(code, node, bit)) return -1;
+
+ node = code->tree[node].branches[bit];
+ }
+
+ return leaf_value(code, node);
+}
diff --git a/lha/huffman.h b/lha/huffman.h
new file mode 100644
index 0000000..c4f3a3e
--- /dev/null
+++ b/lha/huffman.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_HUFFMAN_H_
+#define AARU_COMPRESSION_NATIVE__LHA_HUFFMAN_H_
+
+#include
+#include
+#include "bitio.h"
+
+#define LHA_HUFFMAN_TABLE_SIZE 10
+
+typedef struct
+{
+ int branches[2];
+} lha_tree_node;
+
+typedef struct
+{
+ uint32_t length;
+ int32_t value;
+} lha_table_entry;
+
+typedef struct lha_prefix_code
+{
+ lha_tree_node *tree;
+ int numentries;
+ int minlength;
+ int maxlength;
+
+ int tablesize;
+ lha_table_entry *table;
+} lha_prefix_code;
+
+lha_prefix_code *lha_prefix_code_new(void);
+lha_prefix_code *lha_prefix_code_from_lengths(const int *lengths, int num_symbols, int max_length,
+ bool shortest_is_zeros);
+void lha_prefix_code_free(lha_prefix_code *code);
+void lha_prefix_code_add(lha_prefix_code *code, int value, uint32_t codebits, int length);
+int lha_prefix_code_decode(lha_bitio *bitio, lha_prefix_code *code);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_HUFFMAN_H_ */
diff --git a/lha/larc.c b/lha/larc.c
new file mode 100644
index 0000000..1573905
--- /dev/null
+++ b/lha/larc.c
@@ -0,0 +1,134 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "larc.h"
+#include "bitio.h"
+#include "lzss.h"
+
+#include
+
+/*
+ * LArc -lzs- decompression.
+ * Window: 2048 bytes.
+ * Bit stream, MSB first.
+ * Flag bit 1 = literal (8 bits), flag bit 0 = match (11-bit offset + 4-bit length).
+ */
+AARU_EXPORT int AARU_CALL larc_decompress_lzs(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lha_bitio bitio;
+ lha_lzss lzss;
+ size_t expected;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ expected = *out_len;
+
+ if(!lha_lzss_init(&lzss, 2048, out_buf, expected)) return -1;
+
+ lha_bitio_init(&bitio, in_buf, in_len);
+
+ while(lzss.out_pos < expected && !lha_bitio_at_eof(&bitio))
+ {
+ if(lha_bitio_next_bit(&bitio))
+ {
+ /* Literal byte */
+ uint8_t literal = (uint8_t)lha_bitio_next_bits(&bitio, 8);
+ lha_lzss_emit_literal(&lzss, literal);
+ }
+ else
+ {
+ /* Match */
+ int raw_offset = (int)lha_bitio_next_bits(&bitio, 11);
+ int length = (int)lha_bitio_next_bits(&bitio, 4) + 2;
+ int offset = (int)lzss.position - raw_offset - 17;
+
+ lha_lzss_emit_match(&lzss, offset, length);
+ }
+ }
+
+ *out_len = lzss.out_pos;
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
+
+/*
+ * LArc -lz5- decompression.
+ * Window: 4096 bytes, pre-filled with specific byte pattern.
+ * Flag-byte stream: each flag byte controls 8 items.
+ * Flag bit set = literal (1 byte), flag bit clear = match (2 bytes).
+ */
+AARU_EXPORT int AARU_CALL larc_decompress_lz5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lha_lzss lzss;
+ size_t expected;
+ size_t in_pos;
+ int i;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ expected = *out_len;
+
+ if(!lha_lzss_init(&lzss, 4096, out_buf, expected)) return -1;
+
+ /* Pre-fill window with specific pattern (same as lharc -lh1-) */
+ for(i = 0; i < 256; i++) memset(&lzss.window[i * 13 + 18], i, 13);
+
+ for(i = 0; i < 256; i++) lzss.window[256 * 13 + 18 + i] = (uint8_t)i;
+
+ for(i = 0; i < 256; i++) lzss.window[256 * 13 + 256 + 18 + i] = (uint8_t)(255 - i);
+
+ memset(&lzss.window[256 * 13 + 512 + 18], 0, 128);
+ memset(&lzss.window[256 * 13 + 512 + 128 + 18], ' ', 128 - 18);
+
+ in_pos = 0;
+
+ while(lzss.out_pos < expected && in_pos < in_len)
+ {
+ uint8_t flags = in_buf[in_pos++];
+ int bit;
+
+ for(bit = 0; bit < 8 && lzss.out_pos < expected && in_pos < in_len; bit++)
+ {
+ if(flags & (1 << bit))
+ {
+ /* Literal */
+ lha_lzss_emit_literal(&lzss, in_buf[in_pos++]);
+ }
+ else
+ {
+ /* Match: 2 bytes */
+ uint8_t byte1, byte2;
+ int offset, length;
+
+ if(in_pos + 1 >= in_len) break;
+
+ byte1 = in_buf[in_pos++];
+ byte2 = in_buf[in_pos++];
+
+ offset = (int)lzss.position - (int)byte1 - ((byte2 & 0xf0) << 4) - 18;
+ length = (byte2 & 0x0f) + 3;
+
+ lha_lzss_emit_match(&lzss, offset, length);
+ }
+ }
+ }
+
+ *out_len = lzss.out_pos;
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
diff --git a/lha/larc.h b/lha/larc.h
new file mode 100644
index 0000000..54af3e5
--- /dev/null
+++ b/lha/larc.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_LARC_H_
+#define AARU_COMPRESSION_NATIVE__LHA_LARC_H_
+
+#include
+#include
+#include "../library.h"
+
+AARU_EXPORT int AARU_CALL larc_decompress_lzs(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL larc_decompress_lz5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_LARC_H_ */
diff --git a/lha/lh1.c b/lha/lh1.c
new file mode 100644
index 0000000..f5305a9
--- /dev/null
+++ b/lha/lh1.c
@@ -0,0 +1,280 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "lh1.h"
+#include "bitio.h"
+#include "huffman.h"
+#include "lzss.h"
+
+#include
+
+#define LH1_NUM_LEAVES 314
+#define LH1_NUM_NODES (LH1_NUM_LEAVES * 2 - 1)
+
+typedef struct lh1_node
+{
+ struct lh1_node *parent;
+ struct lh1_node *leftchild;
+ struct lh1_node *rightchild;
+ int index; /* Position in sorted nodes[] array */
+ int freq;
+ int value; /* Symbol value for leaf nodes */
+} lh1_node;
+
+typedef struct
+{
+ lh1_node storage[LH1_NUM_NODES];
+ lh1_node *nodes[LH1_NUM_NODES]; /* Sorted by frequency order */
+} lh1_tree;
+
+static void lh1_init_tree(lh1_tree *tree)
+{
+ int i;
+
+ memset(tree->storage, 0, sizeof(tree->storage));
+
+ for(i = 0; i < LH1_NUM_NODES; i++) tree->nodes[i] = &tree->storage[i];
+
+ /* Initialize leaf nodes (at the high end of the sorted array) */
+ for(i = 0; i < LH1_NUM_LEAVES; i++)
+ {
+ int index = LH1_NUM_NODES - 1 - i;
+ tree->nodes[index]->index = index;
+ tree->nodes[index]->freq = 1;
+ tree->nodes[index]->value = i;
+ tree->nodes[index]->leftchild = NULL;
+ tree->nodes[index]->rightchild = NULL;
+ }
+
+ /* Build branch nodes bottom-up */
+ for(i = LH1_NUM_LEAVES - 2; i >= 0; i--)
+ {
+ tree->nodes[i]->index = i;
+ tree->nodes[i]->leftchild = tree->nodes[2 * i + 1];
+ tree->nodes[i]->rightchild = tree->nodes[2 * i + 2];
+ tree->nodes[i]->leftchild->parent = tree->nodes[i];
+ tree->nodes[i]->rightchild->parent = tree->nodes[i];
+ tree->nodes[i]->freq = tree->nodes[i]->leftchild->freq + tree->nodes[i]->rightchild->freq;
+ }
+
+ tree->nodes[0]->parent = NULL;
+}
+
+static void lh1_rearrange(lh1_tree *tree, lh1_node *node)
+{
+ int p_index = node->index;
+ int q_index = p_index;
+ lh1_node *p = node;
+ lh1_node *q;
+
+ /* Find highest position where this node should be */
+ while(q_index > 0 && tree->nodes[q_index - 1]->freq < p->freq) q_index--;
+
+ if(q_index >= p_index) return;
+
+ q = tree->nodes[q_index];
+
+ /* Swap parent-child links using pointers */
+ {
+ lh1_node *new_q_parent = p->parent;
+ lh1_node *new_p_parent = q->parent;
+ int p_is_right = (p->parent && p->parent->rightchild == p);
+ int q_is_right = (q->parent && q->parent->rightchild == q);
+
+ if(p->parent)
+ {
+ if(p_is_right)
+ p->parent->rightchild = q;
+ else
+ p->parent->leftchild = q;
+ }
+
+ if(q->parent)
+ {
+ if(q_is_right)
+ q->parent->rightchild = p;
+ else
+ q->parent->leftchild = p;
+ }
+
+ p->parent = new_p_parent;
+ q->parent = new_q_parent;
+ }
+
+ /* Swap positions in sorted array */
+ tree->nodes[p_index] = q;
+ tree->nodes[p_index]->index = p_index;
+ tree->nodes[q_index] = p;
+ tree->nodes[q_index]->index = q_index;
+}
+
+static void lh1_reconstruct(lh1_tree *tree)
+{
+ lh1_node *leaves[LH1_NUM_LEAVES];
+ int n = 0;
+ int i;
+ int leaf_index, branch_index, node_index, pair_index;
+
+ /* Collect all leaf nodes and halve their frequencies */
+ for(i = 0; i < LH1_NUM_NODES; i++)
+ {
+ if(!tree->nodes[i]->leftchild && !tree->nodes[i]->rightchild)
+ {
+ tree->nodes[i]->freq = (tree->nodes[i]->freq + 1) / 2;
+ leaves[n++] = tree->nodes[i];
+ }
+ }
+
+ leaf_index = LH1_NUM_LEAVES - 1;
+ branch_index = LH1_NUM_LEAVES - 2;
+ node_index = LH1_NUM_NODES - 1;
+ pair_index = LH1_NUM_NODES - 2;
+
+ while(node_index >= 0)
+ {
+ while(node_index >= pair_index)
+ {
+ tree->nodes[node_index] = leaves[leaf_index];
+ tree->nodes[node_index]->index = node_index;
+ node_index--;
+ leaf_index--;
+ }
+
+ {
+ lh1_node *branch = &tree->storage[branch_index--];
+ branch->leftchild = tree->nodes[pair_index];
+ branch->rightchild = tree->nodes[pair_index + 1];
+ branch->leftchild->parent = branch;
+ branch->rightchild->parent = branch;
+ branch->freq = tree->nodes[pair_index]->freq + tree->nodes[pair_index + 1]->freq;
+
+ while(leaf_index >= 0 && leaves[leaf_index]->freq <= branch->freq)
+ {
+ tree->nodes[node_index] = leaves[leaf_index];
+ tree->nodes[node_index]->index = node_index;
+ node_index--;
+ leaf_index--;
+ }
+
+ tree->nodes[node_index] = branch;
+ tree->nodes[node_index]->index = node_index;
+ node_index--;
+ pair_index -= 2;
+ }
+ }
+
+ tree->nodes[0]->parent = NULL;
+}
+
+static void lh1_update(lh1_tree *tree, lh1_node *node)
+{
+ if(tree->nodes[0]->freq == 0x8000) lh1_reconstruct(tree);
+
+ for(;;)
+ {
+ node->freq++;
+
+ if(!node->parent) break;
+
+ lh1_rearrange(tree, node);
+ node = node->parent;
+ }
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lha_bitio bitio;
+ lha_lzss lzss;
+ lh1_tree tree;
+ lha_prefix_code *distancecode;
+ size_t expected;
+ int i;
+
+ static const int dist_lengths[64] = {3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8};
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ expected = *out_len;
+
+ if(!lha_lzss_init(&lzss, 4096, out_buf, expected)) return -1;
+
+ /* Pre-fill window with specific pattern */
+ for(i = 0; i < 256; i++) memset(&lzss.window[i * 13 + 18], i, 13);
+
+ for(i = 0; i < 256; i++) lzss.window[256 * 13 + 18 + i] = (uint8_t)i;
+
+ for(i = 0; i < 256; i++) lzss.window[256 * 13 + 256 + 18 + i] = (uint8_t)(255 - i);
+
+ memset(&lzss.window[256 * 13 + 512 + 18], 0, 128);
+ memset(&lzss.window[256 * 13 + 512 + 128 + 18], ' ', 128 - 18);
+
+ distancecode = lha_prefix_code_from_lengths(dist_lengths, 64, 8, true);
+
+ if(!distancecode)
+ {
+ lha_lzss_cleanup(&lzss);
+ return -1;
+ }
+
+ lha_bitio_init(&bitio, in_buf, in_len);
+ lh1_init_tree(&tree);
+
+ while(lzss.out_pos < expected && !lha_bitio_at_eof(&bitio))
+ {
+ /* Traverse adaptive Huffman tree */
+ lh1_node *node = tree.nodes[0]; /* root */
+
+ while(node->leftchild || node->rightchild)
+ {
+ if(lha_bitio_next_bit(&bitio))
+ node = node->leftchild;
+ else
+ node = node->rightchild;
+
+ if(!node) break;
+ }
+
+ if(!node) break;
+
+ lh1_update(&tree, node);
+
+ if(node->value < 0x100)
+ {
+ /* Literal */
+ lha_lzss_emit_literal(&lzss, (uint8_t)node->value);
+ }
+ else
+ {
+ /* Match */
+ int length = node->value - 0x100 + 3;
+ int highbits = lha_prefix_code_decode(&bitio, distancecode);
+ int lowbits = (int)lha_bitio_next_bits(&bitio, 6);
+ int offset = (highbits << 6) + lowbits + 1;
+
+ lha_lzss_emit_match(&lzss, offset, length);
+ }
+ }
+
+ *out_len = lzss.out_pos;
+ lha_prefix_code_free(distancecode);
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
diff --git a/lha/lh1.h b/lha/lh1.h
new file mode 100644
index 0000000..59b2149
--- /dev/null
+++ b/lha/lh1.h
@@ -0,0 +1,28 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_LH1_H_
+#define AARU_COMPRESSION_NATIVE__LHA_LH1_H_
+
+#include
+#include
+#include "../library.h"
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_LH1_H_ */
diff --git a/lha/lh_old.c b/lha/lh_old.c
new file mode 100644
index 0000000..091ba6e
--- /dev/null
+++ b/lha/lh_old.c
@@ -0,0 +1,1217 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+/*
+ * Self-contained port of LH2, LH3 and PM2 decompression from legacy code.
+ * Uses its own 16-bit bit I/O and Huffman tree management.
+ */
+
+#include "lh_old.h"
+
+#include
+#include
+
+/* --- Constants --- */
+
+#define UCHAR_MAX_VAL 255
+#define CHAR_BIT_VAL 8
+#define USHRT_BIT 16
+#define MAXMATCH 256
+#define NC (UCHAR_MAX_VAL + MAXMATCH + 2 - THRESHOLD)
+#define THRESHOLD 3
+#define NPT 0x80
+#define CBIT 9
+#define TBIT 5
+#define NT (USHRT_BIT + 3)
+#define N_CHAR (256 + 60 - THRESHOLD + 1)
+#define TREESIZE_C (N_CHAR * 2)
+#define TREESIZE_P (128 * 2)
+#define TREESIZE (TREESIZE_C + TREESIZE_P)
+#define ROOT_C 0
+#define ROOT_P TREESIZE_C
+#define N1 286
+#define EXTRABITS 8
+#define BUFBITS 16
+#define NP (MAX_DICBIT + 1)
+#define MAX_DICBIT 16
+#define LENFIELD 4
+#define PMARC2_OFFSET (0x100 - 2)
+
+/* --- Buffer I/O --- */
+
+typedef struct
+{
+ const uint8_t *in_buf;
+ size_t in_len;
+ size_t in_pos;
+ uint8_t *out_buf;
+ size_t out_len;
+ size_t out_pos;
+ int error;
+} lhold_io;
+
+static int lhold_getchar(lhold_io *io)
+{
+ if(io->in_pos >= io->in_len) return 0;
+
+ return io->in_buf[io->in_pos++];
+}
+
+static void lhold_putchar(lhold_io *io, uint8_t c)
+{
+ if(io->out_pos < io->out_len) io->out_buf[io->out_pos++] = c;
+}
+
+static int lhold_at_end(lhold_io *io) { return io->out_pos >= io->out_len; }
+
+/* --- PM2 Tree --- */
+
+struct pmarc2_tree
+{
+ uint8_t *leftarr;
+ uint8_t *rightarr;
+ uint8_t root;
+};
+
+/* --- Decompression data --- */
+
+struct lhold_st
+{
+ int32_t pbit;
+ int32_t np;
+ int32_t nn;
+ int32_t n1;
+ int32_t most_p;
+ int32_t avail;
+ uint32_t n_max;
+ uint16_t maxmatch;
+ uint16_t total_p;
+ uint16_t blocksize;
+ uint16_t c_table[4096];
+ uint16_t pt_table[256];
+ uint16_t left[2 * NC - 1];
+ uint16_t right[2 * NC - 1];
+ uint16_t freq[TREESIZE];
+ uint16_t pt_code[NPT];
+ int16_t child[TREESIZE];
+ int16_t stock[TREESIZE];
+ int16_t s_node[TREESIZE / 2];
+ int16_t block[TREESIZE];
+ int16_t parent[TREESIZE];
+ int16_t edge[TREESIZE];
+ uint8_t c_len[NC];
+ uint8_t pt_len[NPT];
+};
+
+struct lhold_pm
+{
+ struct pmarc2_tree tree1;
+ struct pmarc2_tree tree2;
+ uint16_t lastupdate;
+ uint16_t dicsiz1;
+ uint8_t gettree1;
+ uint8_t tree1left[32];
+ uint8_t tree1right[32];
+ uint8_t table1[32];
+ uint8_t tree2left[8];
+ uint8_t tree2right[8];
+ uint8_t table2[8];
+ uint8_t tree1bound;
+ uint8_t mindepth;
+ uint8_t prev[0x100];
+ uint8_t next[0x100];
+ uint8_t parentarr[0x100];
+ uint8_t lastbyte;
+};
+
+struct lhold_data
+{
+ lhold_io *io;
+ uint8_t *text;
+ uint16_t dicbit;
+
+ uint16_t bitbuf;
+ uint8_t subbitbuf;
+ uint8_t bitcount;
+ uint32_t loc;
+ uint32_t count;
+ uint32_t nextcount;
+
+ union
+ {
+ struct lhold_st st;
+ struct lhold_pm pm;
+ } d;
+};
+
+/* --- Bit I/O (16-bit accumulator) --- */
+
+static void fillbuf(struct lhold_data *dat, uint8_t n)
+{
+ if(dat->io->error) return;
+
+ while(n > dat->bitcount)
+ {
+ n -= dat->bitcount;
+ dat->bitbuf = (uint16_t)((dat->bitbuf << dat->bitcount) + (dat->subbitbuf >> (CHAR_BIT_VAL - dat->bitcount)));
+ dat->subbitbuf = (uint8_t)lhold_getchar(dat->io);
+ dat->bitcount = CHAR_BIT_VAL;
+ }
+
+ dat->bitcount -= n;
+ dat->bitbuf = (uint16_t)((dat->bitbuf << n) + (dat->subbitbuf >> (CHAR_BIT_VAL - n)));
+ dat->subbitbuf <<= n;
+}
+
+static uint16_t getbits(struct lhold_data *dat, uint8_t n)
+{
+ uint16_t x = (uint16_t)(dat->bitbuf >> (2 * CHAR_BIT_VAL - n));
+
+ fillbuf(dat, n);
+ return x;
+}
+
+#define init_getbits(a) fillbuf((a), 2 * CHAR_BIT_VAL)
+
+/* --- Huffman table construction --- */
+
+static void make_table(struct lhold_data *dat, int16_t nchar, uint8_t bitlen[], int16_t tablebits, uint16_t table[])
+{
+ uint16_t count[17];
+ uint16_t weight[17];
+ uint16_t start[17];
+ uint16_t total;
+ uint32_t i;
+ int32_t j, k, l, m, n, avail;
+ uint16_t *p;
+
+ if(dat->io->error) return;
+
+ avail = nchar;
+ memset(count, 0, sizeof(count));
+
+ for(i = 1; i <= 16; i++) weight[i] = (uint16_t)(1 << (16 - i));
+
+ for(i = 0; (int32_t)i < nchar; i++) count[bitlen[i]]++;
+
+ total = 0;
+
+ for(i = 1; i <= 16; i++)
+ {
+ start[i] = total;
+ total = (uint16_t)(total + weight[i] * count[i]);
+ }
+
+ if(total & 0xFFFF)
+ {
+ dat->io->error = 1;
+ return;
+ }
+
+ m = 16 - tablebits;
+
+ for(i = 1; (int32_t)i <= tablebits; i++)
+ {
+ start[i] >>= m;
+ weight[i] >>= m;
+ }
+
+ j = start[tablebits + 1] >> m;
+ k = 1 << tablebits;
+
+ if(j != 0)
+ for(i = (uint32_t)j; (int32_t)i < k; i++) table[i] = 0;
+
+ for(j = 0; j < nchar; j++)
+ {
+ k = bitlen[j];
+
+ if(k == 0) continue;
+
+ l = start[k] + weight[k];
+
+ if(k <= tablebits)
+ {
+ for(i = (uint32_t)start[k]; (int32_t)i < l; i++) table[i] = (uint16_t)j;
+ }
+ else
+ {
+ p = &table[(i = (uint32_t)start[k]) >> m];
+ i <<= tablebits;
+ n = k - tablebits;
+
+ while(--n >= 0)
+ {
+ if(*p == 0)
+ {
+ dat->d.st.right[avail] = dat->d.st.left[avail] = 0;
+ *p = (uint16_t)avail++;
+ }
+
+ if(i & 0x8000)
+ p = &dat->d.st.right[*p];
+ else
+ p = &dat->d.st.left[*p];
+
+ i <<= 1;
+ }
+
+ *p = (uint16_t)j;
+ }
+
+ start[k] = (uint16_t)l;
+ }
+}
+
+/* --- LH2: Dynamic Huffman --- */
+
+static void start_p_dyn(struct lhold_data *dat)
+{
+ dat->d.st.freq[ROOT_P] = 1;
+ dat->d.st.child[ROOT_P] = (int16_t)(~(N_CHAR));
+ dat->d.st.s_node[N_CHAR] = ROOT_P;
+ dat->d.st.edge[dat->d.st.block[ROOT_P] = dat->d.st.stock[dat->d.st.avail++]] = ROOT_P;
+ dat->d.st.most_p = ROOT_P;
+ dat->d.st.total_p = 0;
+ dat->d.st.nn = 1 << dat->dicbit;
+ dat->nextcount = 64;
+}
+
+static void start_c_dyn(struct lhold_data *dat)
+{
+ int32_t i, j, f;
+
+ dat->d.st.n1 = (int32_t)((dat->d.st.n_max >= (uint32_t)(256 + dat->d.st.maxmatch - THRESHOLD + 1)) ? 512
+ : dat->d.st.n_max - 1);
+
+ for(i = 0; i < TREESIZE_C; i++)
+ {
+ dat->d.st.stock[i] = (int16_t)i;
+ dat->d.st.block[i] = 0;
+ }
+
+ for(i = 0, j = (int32_t)(dat->d.st.n_max * 2 - 2); i < (int32_t)dat->d.st.n_max; i++, j--)
+ {
+ dat->d.st.freq[j] = 1;
+ dat->d.st.child[j] = (int16_t)(~i);
+ dat->d.st.s_node[i] = (int16_t)j;
+ dat->d.st.block[j] = 1;
+ }
+
+ dat->d.st.avail = 2;
+ dat->d.st.edge[1] = (int16_t)(dat->d.st.n_max - 1);
+ i = (int32_t)(dat->d.st.n_max * 2 - 2);
+
+ while(j >= 0)
+ {
+ f = dat->d.st.freq[j] = dat->d.st.freq[i] + dat->d.st.freq[i - 1];
+ dat->d.st.child[j] = (int16_t)i;
+ dat->d.st.parent[i] = dat->d.st.parent[i - 1] = (int16_t)j;
+
+ if(f == dat->d.st.freq[j + 1])
+ dat->d.st.edge[dat->d.st.block[j] = dat->d.st.block[j + 1]] = (int16_t)j;
+ else
+ dat->d.st.edge[dat->d.st.block[j] = dat->d.st.stock[dat->d.st.avail++]] = (int16_t)j;
+
+ i -= 2;
+ j--;
+ }
+}
+
+static void decode_start_dyn(struct lhold_data *dat)
+{
+ dat->d.st.n_max = 286;
+ dat->d.st.maxmatch = MAXMATCH;
+ init_getbits(dat);
+ start_c_dyn(dat);
+ start_p_dyn(dat);
+}
+
+static void reconst(struct lhold_data *dat, int32_t start, int32_t end)
+{
+ int32_t i, j, k, l, b = 0;
+ uint32_t f, g;
+
+ for(i = j = start; i < end; i++)
+ {
+ if((k = dat->d.st.child[i]) < 0)
+ {
+ dat->d.st.freq[j] = (uint16_t)((dat->d.st.freq[i] + 1) / 2);
+ dat->d.st.child[j] = (int16_t)k;
+ j++;
+ }
+
+ if(dat->d.st.edge[b = dat->d.st.block[i]] == i) dat->d.st.stock[--dat->d.st.avail] = (int16_t)b;
+ }
+
+ j--;
+ i = end - 1;
+ l = end - 2;
+
+ while(i >= start)
+ {
+ while(i >= l)
+ {
+ dat->d.st.freq[i] = dat->d.st.freq[j];
+ dat->d.st.child[i] = dat->d.st.child[j];
+ i--;
+ j--;
+ }
+
+ f = (uint32_t)(dat->d.st.freq[l] + dat->d.st.freq[l + 1]);
+
+ for(k = start; f < (uint32_t)dat->d.st.freq[k]; k++)
+ ;
+
+ while(j >= k)
+ {
+ dat->d.st.freq[i] = dat->d.st.freq[j];
+ dat->d.st.child[i] = dat->d.st.child[j];
+ i--;
+ j--;
+ }
+
+ dat->d.st.freq[i] = (uint16_t)f;
+ dat->d.st.child[i] = (int16_t)(l + 1);
+ i--;
+ l -= 2;
+ }
+
+ f = 0;
+
+ for(i = start; i < end; i++)
+ {
+ if((j = dat->d.st.child[i]) < 0)
+ dat->d.st.s_node[~j] = (int16_t)i;
+ else
+ dat->d.st.parent[j] = dat->d.st.parent[j - 1] = (int16_t)i;
+
+ if((g = (uint32_t)dat->d.st.freq[i]) == f)
+ dat->d.st.block[i] = (int16_t)b;
+ else
+ {
+ dat->d.st.edge[b = dat->d.st.block[i] = dat->d.st.stock[dat->d.st.avail++]] = (int16_t)i;
+ f = g;
+ }
+ }
+}
+
+static int32_t swap_inc(struct lhold_data *dat, int32_t p)
+{
+ int32_t b, q, r, s;
+
+ b = dat->d.st.block[p];
+
+ if((q = dat->d.st.edge[b]) != p)
+ {
+ r = dat->d.st.child[p];
+ s = dat->d.st.child[q];
+ dat->d.st.child[p] = (int16_t)s;
+ dat->d.st.child[q] = (int16_t)r;
+
+ if(r >= 0)
+ dat->d.st.parent[r] = dat->d.st.parent[r - 1] = (int16_t)q;
+ else
+ dat->d.st.s_node[~r] = (int16_t)q;
+
+ if(s >= 0)
+ dat->d.st.parent[s] = dat->d.st.parent[s - 1] = (int16_t)p;
+ else
+ dat->d.st.s_node[~s] = (int16_t)p;
+
+ p = q;
+ dat->d.st.edge[b]++;
+
+ if(++dat->d.st.freq[p] == dat->d.st.freq[p - 1])
+ dat->d.st.block[p] = dat->d.st.block[p - 1];
+ else
+ dat->d.st.edge[dat->d.st.block[p] = dat->d.st.stock[dat->d.st.avail++]] = (int16_t)p;
+ }
+ else if(b == dat->d.st.block[p + 1])
+ {
+ dat->d.st.edge[b]++;
+
+ if(++dat->d.st.freq[p] == dat->d.st.freq[p - 1])
+ dat->d.st.block[p] = dat->d.st.block[p - 1];
+ else
+ dat->d.st.edge[dat->d.st.block[p] = dat->d.st.stock[dat->d.st.avail++]] = (int16_t)p;
+ }
+ else if(++dat->d.st.freq[p] == dat->d.st.freq[p - 1])
+ {
+ dat->d.st.stock[--dat->d.st.avail] = (int16_t)b;
+ dat->d.st.block[p] = dat->d.st.block[p - 1];
+ }
+
+ return dat->d.st.parent[p];
+}
+
+static void update_p(struct lhold_data *dat, int32_t p)
+{
+ int32_t q;
+
+ if(dat->d.st.total_p == 0x8000)
+ {
+ reconst(dat, ROOT_P, dat->d.st.most_p + 1);
+ dat->d.st.total_p = dat->d.st.freq[ROOT_P];
+ dat->d.st.freq[ROOT_P] = 0xffff;
+ }
+
+ q = dat->d.st.s_node[p + N_CHAR];
+
+ while(q != ROOT_P) q = swap_inc(dat, q);
+
+ dat->d.st.total_p++;
+}
+
+static void make_new_node(struct lhold_data *dat, int32_t p)
+{
+ int32_t q, r;
+
+ r = dat->d.st.most_p + 1;
+ q = r + 1;
+ dat->d.st.s_node[~(dat->d.st.child[r] = dat->d.st.child[dat->d.st.most_p])] = (int16_t)r;
+ dat->d.st.child[q] = (int16_t)(~(p + N_CHAR));
+ dat->d.st.child[dat->d.st.most_p] = (int16_t)q;
+ dat->d.st.freq[r] = dat->d.st.freq[dat->d.st.most_p];
+ dat->d.st.freq[q] = 0;
+ dat->d.st.block[r] = dat->d.st.block[dat->d.st.most_p];
+
+ if(dat->d.st.most_p == ROOT_P)
+ {
+ dat->d.st.freq[ROOT_P] = 0xffff;
+ dat->d.st.edge[dat->d.st.block[ROOT_P]]++;
+ }
+
+ dat->d.st.parent[r] = dat->d.st.parent[q] = (int16_t)dat->d.st.most_p;
+ dat->d.st.edge[dat->d.st.block[q] = dat->d.st.stock[dat->d.st.avail++]] = dat->d.st.s_node[p + N_CHAR] =
+ (int16_t)(dat->d.st.most_p = q);
+ update_p(dat, p);
+}
+
+static void update_c(struct lhold_data *dat, int32_t p)
+{
+ int32_t q;
+
+ if(dat->d.st.freq[ROOT_C] == 0x8000) reconst(dat, 0, (int32_t)dat->d.st.n_max * 2 - 1);
+
+ dat->d.st.freq[ROOT_C]++;
+ q = dat->d.st.s_node[p];
+
+ do { q = swap_inc(dat, q); } while(q != ROOT_C);
+}
+
+static uint16_t decode_c_dyn(struct lhold_data *dat)
+{
+ int32_t c;
+ int16_t buf, cnt;
+
+ c = dat->d.st.child[ROOT_C];
+ buf = (int16_t)dat->bitbuf;
+ cnt = 0;
+
+ do
+ {
+ c = dat->d.st.child[c - (buf < 0)];
+ buf <<= 1;
+
+ if(++cnt == 16)
+ {
+ fillbuf(dat, 16);
+ buf = (int16_t)dat->bitbuf;
+ cnt = 0;
+ }
+ } while(c > 0);
+
+ fillbuf(dat, (uint8_t)cnt);
+ c = ~c;
+ update_c(dat, c);
+
+ if(c == dat->d.st.n1) c += getbits(dat, 8);
+
+ return (uint16_t)c;
+}
+
+static uint16_t decode_p_dyn(struct lhold_data *dat)
+{
+ int32_t c;
+ int16_t buf, cnt;
+
+ while(dat->count > dat->nextcount)
+ {
+ make_new_node(dat, (int32_t)dat->nextcount / 64);
+
+ if((dat->nextcount += 64) >= (uint32_t)dat->d.st.nn) dat->nextcount = 0xffffffff;
+ }
+
+ c = dat->d.st.child[ROOT_P];
+ buf = (int16_t)dat->bitbuf;
+ cnt = 0;
+
+ while(c > 0)
+ {
+ c = dat->d.st.child[c - (buf < 0)];
+ buf <<= 1;
+
+ if(++cnt == 16)
+ {
+ fillbuf(dat, 16);
+ buf = (int16_t)dat->bitbuf;
+ cnt = 0;
+ }
+ }
+
+ fillbuf(dat, (uint8_t)cnt);
+ c = (~c) - N_CHAR;
+ update_p(dat, c);
+
+ return (uint16_t)((c << 6) + getbits(dat, 6));
+}
+
+/* --- LH3: Static Huffman --- */
+
+static const int32_t fixed_tables[2][16] = {{3, 0x01, 0x04, 0x0c, 0x18, 0x30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {2, 0x01, 0x01, 0x03, 0x06, 0x0D, 0x1F, 0x4E, 0, 0, 0, 0, 0, 0, 0, 0}};
+
+static void ready_made(struct lhold_data *dat, int32_t method)
+{
+ int32_t i, j;
+ uint32_t code, weight_val;
+ const int32_t *tbl;
+
+ tbl = fixed_tables[method];
+ j = *tbl++;
+ weight_val = (uint32_t)(1 << (16 - j));
+ code = 0;
+
+ for(i = 0; i < dat->d.st.np; i++)
+ {
+ while(*tbl == i)
+ {
+ j++;
+ tbl++;
+ weight_val >>= 1;
+ }
+
+ dat->d.st.pt_len[i] = (uint8_t)j;
+ dat->d.st.pt_code[i] = (uint16_t)code;
+ code += weight_val;
+ }
+}
+
+static uint16_t decode_p_st0(struct lhold_data *dat)
+{
+ int32_t i, j;
+
+ j = dat->d.st.pt_table[dat->bitbuf >> 8];
+
+ if(j < dat->d.st.np)
+ {
+ fillbuf(dat, dat->d.st.pt_len[j]);
+ }
+ else
+ {
+ fillbuf(dat, 8);
+ i = dat->bitbuf;
+
+ do
+ {
+ if((int16_t)i < 0)
+ j = dat->d.st.right[j];
+ else
+ j = dat->d.st.left[j];
+
+ i <<= 1;
+ } while(j >= dat->d.st.np);
+
+ fillbuf(dat, (uint8_t)(dat->d.st.pt_len[j] - 8));
+ }
+
+ return (uint16_t)((j << 6) + getbits(dat, 6));
+}
+
+static void decode_start_st0(struct lhold_data *dat)
+{
+ dat->d.st.n_max = 286;
+ dat->d.st.maxmatch = MAXMATCH;
+ init_getbits(dat);
+ dat->d.st.np = 1 << (MAX_DICBIT - 6);
+}
+
+static void read_tree_c(struct lhold_data *dat)
+{
+ int32_t i, c;
+
+ i = 0;
+
+ while(i < N1)
+ {
+ if(getbits(dat, 1))
+ dat->d.st.c_len[i] = (uint8_t)(getbits(dat, LENFIELD) + 1);
+ else
+ dat->d.st.c_len[i] = 0;
+
+ if(++i == 3 && dat->d.st.c_len[0] == 1 && dat->d.st.c_len[1] == 1 && dat->d.st.c_len[2] == 1)
+ {
+ c = getbits(dat, CBIT);
+ memset(dat->d.st.c_len, 0, N1);
+
+ for(i = 0; i < 4096; i++) dat->d.st.c_table[i] = (uint16_t)c;
+
+ return;
+ }
+ }
+
+ make_table(dat, N1, dat->d.st.c_len, 12, dat->d.st.c_table);
+}
+
+static void read_tree_p(struct lhold_data *dat)
+{
+ int32_t i, c;
+
+ i = 0;
+
+ while(i < NP)
+ {
+ dat->d.st.pt_len[i] = (uint8_t)getbits(dat, LENFIELD);
+
+ if(++i == 3 && dat->d.st.pt_len[0] == 1 && dat->d.st.pt_len[1] == 1 && dat->d.st.pt_len[2] == 1)
+ {
+ c = getbits(dat, MAX_DICBIT - 6);
+
+ for(i = 0; i < NP; i++) dat->d.st.c_len[i] = 0;
+
+ for(i = 0; i < 256; i++) dat->d.st.c_table[i] = (uint16_t)c;
+
+ return;
+ }
+ }
+}
+
+static uint16_t decode_c_st0(struct lhold_data *dat)
+{
+ int32_t i, j;
+
+ if(!dat->d.st.blocksize)
+ {
+ dat->d.st.blocksize = getbits(dat, BUFBITS);
+ read_tree_c(dat);
+
+ if(getbits(dat, 1))
+ read_tree_p(dat);
+ else
+ ready_made(dat, 1);
+
+ make_table(dat, NP, dat->d.st.pt_len, 8, dat->d.st.pt_table);
+ }
+
+ dat->d.st.blocksize--;
+ j = dat->d.st.c_table[dat->bitbuf >> 4];
+
+ if(j < N1)
+ {
+ fillbuf(dat, dat->d.st.c_len[j]);
+ }
+ else
+ {
+ fillbuf(dat, 12);
+ i = dat->bitbuf;
+
+ do
+ {
+ if((int16_t)i < 0)
+ j = dat->d.st.right[j];
+ else
+ j = dat->d.st.left[j];
+
+ i <<= 1;
+ } while(j >= N1);
+
+ fillbuf(dat, (uint8_t)(dat->d.st.c_len[j] - 12));
+ }
+
+ if(j == N1 - 1) j += getbits(dat, EXTRABITS);
+
+ return (uint16_t)j;
+}
+
+/* --- PM2: PMarc v2 --- */
+
+static const int32_t pmarc2_historyBits[8] = {3, 3, 4, 5, 5, 5, 6, 6};
+static const int32_t pmarc2_historyBase[8] = {0, 8, 16, 32, 64, 96, 128, 192};
+static const int32_t pmarc2_repeatBits[6] = {3, 3, 5, 6, 7, 0};
+static const int32_t pmarc2_repeatBase[6] = {17, 25, 33, 65, 129, 256};
+
+static void pmarc2_hist_update(struct lhold_data *dat, uint8_t data)
+{
+ if(data != dat->d.pm.lastbyte)
+ {
+ uint8_t oldNext, oldPrev, newNext;
+
+ oldNext = dat->d.pm.next[data];
+ oldPrev = dat->d.pm.prev[data];
+ dat->d.pm.prev[oldNext] = oldPrev;
+ dat->d.pm.next[oldPrev] = oldNext;
+
+ newNext = dat->d.pm.next[dat->d.pm.lastbyte];
+ dat->d.pm.prev[newNext] = data;
+ dat->d.pm.next[data] = newNext;
+
+ dat->d.pm.prev[data] = dat->d.pm.lastbyte;
+ dat->d.pm.next[dat->d.pm.lastbyte] = data;
+
+ dat->d.pm.lastbyte = data;
+ }
+}
+
+static int32_t pmarc2_tree_get(struct lhold_data *dat, struct pmarc2_tree *t)
+{
+ int32_t i = t->root;
+
+ while(i < 0x80) i = (getbits(dat, 1) == 0 ? t->leftarr[i] : t->rightarr[i]);
+
+ return i & 0x7F;
+}
+
+static void pmarc2_tree_rebuild(struct lhold_data *dat, struct pmarc2_tree *t, uint8_t bound, uint8_t mindepth,
+ uint8_t *table)
+{
+ uint8_t d;
+ int32_t i, curr, empty_node, n;
+
+ t->root = 0;
+ memset(t->leftarr, 0, bound);
+ memset(t->rightarr, 0, bound);
+ memset(dat->d.pm.parentarr, 0, bound);
+
+ for(i = 0; i < dat->d.pm.mindepth - 1; i++)
+ {
+ t->leftarr[i] = (uint8_t)(i + 1);
+ dat->d.pm.parentarr[i + 1] = (uint8_t)i;
+ }
+
+ curr = dat->d.pm.mindepth - 1;
+ empty_node = dat->d.pm.mindepth;
+
+ for(d = dat->d.pm.mindepth;; d++)
+ {
+ for(i = 0; i < bound; i++)
+ {
+ if(table[i] == d)
+ {
+ if(t->leftarr[curr] == 0)
+ {
+ t->leftarr[curr] = (uint8_t)(i | 128);
+ }
+ else
+ {
+ t->rightarr[curr] = (uint8_t)(i | 128);
+ n = 0;
+
+ while(t->rightarr[curr] != 0)
+ {
+ if(curr == 0) return;
+
+ curr = dat->d.pm.parentarr[curr];
+ n++;
+ }
+
+ t->rightarr[curr] = (uint8_t)empty_node;
+
+ for(;;)
+ {
+ dat->d.pm.parentarr[empty_node] = (uint8_t)curr;
+ curr = empty_node;
+ empty_node++;
+ n--;
+
+ if(n == 0) break;
+
+ t->leftarr[curr] = (uint8_t)empty_node;
+ }
+ }
+ }
+ }
+
+ if(t->leftarr[curr] == 0)
+ t->leftarr[curr] = (uint8_t)empty_node;
+ else
+ t->rightarr[curr] = (uint8_t)empty_node;
+
+ dat->d.pm.parentarr[empty_node] = (uint8_t)curr;
+ curr = empty_node;
+ empty_node++;
+ }
+}
+
+static uint8_t pmarc2_hist_lookup(struct lhold_data *dat, int32_t n)
+{
+ uint8_t i;
+ uint8_t *direction = dat->d.pm.prev;
+
+ if(n >= 0x80)
+ {
+ direction = dat->d.pm.next;
+ n = 0x100 - n;
+ }
+
+ for(i = dat->d.pm.lastbyte; n != 0; n--) i = direction[i];
+
+ return i;
+}
+
+static void pmarc2_maketree1(struct lhold_data *dat)
+{
+ int32_t i, nbits, x;
+
+ dat->d.pm.tree1bound = (uint8_t)getbits(dat, 5);
+ dat->d.pm.mindepth = (uint8_t)getbits(dat, 3);
+
+ if(dat->d.pm.mindepth == 0)
+ {
+ dat->d.pm.tree1.root = (uint8_t)(128 | (dat->d.pm.tree1bound - 1));
+ }
+ else
+ {
+ memset(dat->d.pm.table1, 0, 32);
+ nbits = getbits(dat, 3);
+
+ for(i = 0; i < dat->d.pm.tree1bound; i++)
+ {
+ if((x = getbits(dat, (uint8_t)nbits))) dat->d.pm.table1[i] = (uint8_t)(x - 1 + dat->d.pm.mindepth);
+ }
+
+ pmarc2_tree_rebuild(dat, &dat->d.pm.tree1, dat->d.pm.tree1bound, dat->d.pm.mindepth, dat->d.pm.table1);
+ }
+}
+
+static void pmarc2_maketree2(struct lhold_data *dat, int32_t par_b)
+{
+ int32_t i, count, index;
+
+ if(dat->d.pm.tree1bound < 10) return;
+
+ if(dat->d.pm.tree1bound == 29 && dat->d.pm.mindepth == 0) return;
+
+ for(i = 0; i < 8; i++) dat->d.pm.table2[i] = 0;
+
+ for(i = 0; i < par_b; i++) dat->d.pm.table2[i] = (uint8_t)getbits(dat, 3);
+
+ index = 0;
+ count = 0;
+
+ for(i = 0; i < 8; i++)
+ {
+ if(dat->d.pm.table2[i] != 0)
+ {
+ index = i;
+ count++;
+ }
+ }
+
+ if(count == 1)
+ dat->d.pm.tree2.root = (uint8_t)(128 | index);
+ else if(count > 1)
+ {
+ dat->d.pm.mindepth = 1;
+ pmarc2_tree_rebuild(dat, &dat->d.pm.tree2, 8, dat->d.pm.mindepth, dat->d.pm.table2);
+ }
+}
+
+static void decode_start_pm2(struct lhold_data *dat)
+{
+ int32_t i;
+
+ dat->d.pm.tree1.leftarr = dat->d.pm.tree1left;
+ dat->d.pm.tree1.rightarr = dat->d.pm.tree1right;
+ dat->d.pm.tree2.leftarr = dat->d.pm.tree2left;
+ dat->d.pm.tree2.rightarr = dat->d.pm.tree2right;
+
+ dat->d.pm.dicsiz1 = (uint16_t)((1 << dat->dicbit) - 1);
+ init_getbits(dat);
+
+ /* Initialize history list */
+ for(i = 0; i < 0x100; i++)
+ {
+ dat->d.pm.prev[(0xFF + i) & 0xFF] = (uint8_t)i;
+ dat->d.pm.next[(0x01 + i) & 0xFF] = (uint8_t)i;
+ }
+
+ dat->d.pm.prev[0x7F] = 0x00;
+ dat->d.pm.next[0x00] = 0x7F;
+ dat->d.pm.prev[0xDF] = 0x80;
+ dat->d.pm.next[0x80] = 0xDF;
+ dat->d.pm.prev[0x9F] = 0xE0;
+ dat->d.pm.next[0xE0] = 0x9F;
+ dat->d.pm.prev[0x1F] = 0xA0;
+ dat->d.pm.next[0xA0] = 0x1F;
+ dat->d.pm.prev[0xFF] = 0x20;
+ dat->d.pm.next[0x20] = 0xFF;
+ dat->d.pm.lastbyte = 0x20;
+
+ getbits(dat, 1); /* discard bit */
+}
+
+static uint16_t decode_c_pm2(struct lhold_data *dat)
+{
+ while(dat->d.pm.lastupdate != dat->loc)
+ {
+ pmarc2_hist_update(dat, dat->text[dat->d.pm.lastupdate]);
+ dat->d.pm.lastupdate = (uint16_t)((dat->d.pm.lastupdate + 1) & dat->d.pm.dicsiz1);
+ }
+
+ while(dat->count >= dat->nextcount)
+ {
+ if(dat->nextcount == 0x0000)
+ {
+ pmarc2_maketree1(dat);
+ pmarc2_maketree2(dat, 5);
+ dat->nextcount = 0x0400;
+ }
+ else if(dat->nextcount == 0x0400)
+ {
+ pmarc2_maketree2(dat, 6);
+ dat->nextcount = 0x0800;
+ }
+ else if(dat->nextcount == 0x0800)
+ {
+ pmarc2_maketree2(dat, 7);
+ dat->nextcount = 0x1000;
+ }
+ else if(dat->nextcount == 0x1000)
+ {
+ if(getbits(dat, 1) != 0) pmarc2_maketree1(dat);
+
+ pmarc2_maketree2(dat, 8);
+ dat->nextcount = 0x2000;
+ }
+ else
+ {
+ if(getbits(dat, 1) != 0)
+ {
+ pmarc2_maketree1(dat);
+ pmarc2_maketree2(dat, 8);
+ }
+
+ dat->nextcount += 0x1000;
+ }
+ }
+
+ dat->d.pm.gettree1 = (uint8_t)pmarc2_tree_get(dat, &dat->d.pm.tree1);
+
+ if(dat->d.pm.gettree1 < 8)
+ return (uint16_t)(pmarc2_hist_lookup(dat,
+ pmarc2_historyBase[dat->d.pm.gettree1] +
+ getbits(dat, (uint8_t)pmarc2_historyBits[dat->d.pm.gettree1])));
+
+ if(dat->d.pm.gettree1 < 23) return (uint16_t)(PMARC2_OFFSET + 2 + (dat->d.pm.gettree1 - 8));
+
+ return (uint16_t)(PMARC2_OFFSET + pmarc2_repeatBase[dat->d.pm.gettree1 - 23] +
+ getbits(dat, (uint8_t)pmarc2_repeatBits[dat->d.pm.gettree1 - 23]));
+}
+
+static uint16_t decode_p_pm2(struct lhold_data *dat)
+{
+ int32_t nbits, delta, gettree2;
+
+ if(dat->d.pm.gettree1 == 8)
+ {
+ nbits = 6;
+ delta = 0;
+ }
+ else if(dat->d.pm.gettree1 < 28)
+ {
+ if(!(gettree2 = pmarc2_tree_get(dat, &dat->d.pm.tree2)))
+ {
+ nbits = 6;
+ delta = 0;
+ }
+ else
+ {
+ nbits = 5 + gettree2;
+ delta = 1 << nbits;
+ }
+ }
+ else
+ {
+ nbits = 0;
+ delta = 0;
+ }
+
+ return (uint16_t)(delta + getbits(dat, (uint8_t)nbits));
+}
+
+/* --- Main decompression dispatcher --- */
+
+#define METHOD_LH2 0
+#define METHOD_LH3 1
+#define METHOD_PM2 2
+
+static int lhold_decrunch(lhold_io *io, int method)
+{
+ struct lhold_data *dd;
+ int result = -1;
+
+ dd = (struct lhold_data *)calloc(1, sizeof(struct lhold_data));
+
+ if(!dd) return -1;
+
+ dd->io = io;
+ dd->dicbit = 13;
+
+ {
+ void (*decodeStart)(struct lhold_data *) = NULL;
+ uint16_t (*decodeC)(struct lhold_data *) = NULL;
+ uint16_t (*decodeP)(struct lhold_data *) = NULL;
+
+ switch(method)
+ {
+ case METHOD_LH2:
+ decodeStart = decode_start_dyn;
+ decodeC = decode_c_dyn;
+ decodeP = decode_p_dyn;
+ break;
+ case METHOD_LH3:
+ decodeStart = decode_start_st0;
+ decodeP = decode_p_st0;
+ decodeC = decode_c_st0;
+ break;
+ case METHOD_PM2:
+ decodeStart = decode_start_pm2;
+ decodeP = decode_p_pm2;
+ decodeC = decode_c_pm2;
+ break;
+ default: free(dd); return -1;
+ }
+
+ {
+ uint32_t dicsiz = (uint32_t)(1 << dd->dicbit);
+ int32_t offset = (method == METHOD_PM2) ? 0x100 - 2 : 0x100 - 3;
+
+ dd->text = (uint8_t *)calloc(dicsiz, 1);
+
+ if(!dd->text)
+ {
+ free(dd);
+ return -1;
+ }
+
+ memset(dd->text, ' ', dicsiz);
+
+ decodeStart(dd);
+ dicsiz--; /* now used with AND */
+
+ while(!lhold_at_end(io) && !io->error)
+ {
+ int32_t c = decodeC(dd);
+
+ if(io->error) break;
+
+ if(c <= UCHAR_MAX_VAL)
+ {
+ dd->text[dd->loc++] = (uint8_t)c;
+ lhold_putchar(io, (uint8_t)c);
+ dd->loc &= dicsiz;
+ dd->count++;
+ }
+ else
+ {
+ int32_t i;
+ uint16_t p;
+
+ c -= offset;
+ p = decodeP(dd);
+
+ if(io->error) break;
+
+ i = (int32_t)(dd->loc - p - 1);
+ dd->count += (uint32_t)c;
+
+ while(c--)
+ {
+ uint8_t byte = dd->text[i++ & dicsiz];
+ dd->text[dd->loc++] = byte;
+ lhold_putchar(io, byte);
+ dd->loc &= dicsiz;
+ }
+ }
+ }
+
+ result = io->error ? -1 : 0;
+ free(dd->text);
+ }
+ }
+
+ free(dd);
+ return result;
+}
+
+/* --- Public API --- */
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lhold_io io;
+ int result;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ memset(&io, 0, sizeof(io));
+ io.in_buf = in_buf;
+ io.in_len = in_len;
+ io.out_buf = out_buf;
+ io.out_len = *out_len;
+
+ result = lhold_decrunch(&io, METHOD_LH2);
+ *out_len = io.out_pos;
+ return result;
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh3(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lhold_io io;
+ int result;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ memset(&io, 0, sizeof(io));
+ io.in_buf = in_buf;
+ io.in_len = in_len;
+ io.out_buf = out_buf;
+ io.out_len = *out_len;
+
+ result = lhold_decrunch(&io, METHOD_LH3);
+ *out_len = io.out_pos;
+ return result;
+}
+
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ lhold_io io;
+ int result;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ memset(&io, 0, sizeof(io));
+ io.in_buf = in_buf;
+ io.in_len = in_len;
+ io.out_buf = out_buf;
+ io.out_len = *out_len;
+
+ result = lhold_decrunch(&io, METHOD_PM2);
+ *out_len = io.out_pos;
+ return result;
+}
diff --git a/lha/lh_old.h b/lha/lh_old.h
new file mode 100644
index 0000000..ad9a867
--- /dev/null
+++ b/lha/lh_old.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_LH_OLD_H_
+#define AARU_COMPRESSION_NATIVE__LHA_LH_OLD_H_
+
+#include
+#include
+#include "../library.h"
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh3(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_LH_OLD_H_ */
diff --git a/lha/lh_static.c b/lha/lh_static.c
new file mode 100644
index 0000000..21b8eda
--- /dev/null
+++ b/lha/lh_static.c
@@ -0,0 +1,252 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "lh_static.h"
+#include "bitio.h"
+#include "huffman.h"
+#include "lzss.h"
+
+#include
+#include
+
+/*
+ * Parse a generic Huffman code from the bitstream.
+ * Width = number of bits for the symbol count.
+ * specialindex = index at which to insert extra zero-run bits (-1 for none).
+ */
+static lha_prefix_code *parse_code(lha_bitio *bitio, int width, int specialindex)
+{
+ int num = (int)lha_bitio_next_bits(bitio, width);
+
+ if(num == 0)
+ {
+ /* Single symbol */
+ int val = (int)lha_bitio_next_bits(bitio, width);
+ lha_prefix_code *code = lha_prefix_code_new();
+
+ if(!code) return NULL;
+
+ lha_prefix_code_add(code, val, 0, 0);
+ return code;
+ }
+ else
+ {
+ int *codelengths = (int *)calloc((size_t)num, sizeof(int));
+ int n = 0;
+ lha_prefix_code *code;
+
+ if(!codelengths) return NULL;
+
+ while(n < num)
+ {
+ int len = (int)lha_bitio_next_bits(bitio, 3);
+
+ if(len == 7)
+ while(lha_bitio_next_bit(bitio)) len++;
+
+ codelengths[n++] = len;
+
+ if(n == specialindex)
+ {
+ int zeroes = (int)lha_bitio_next_bits(bitio, 2);
+ int i;
+
+ for(i = 0; i < zeroes && n < num; i++) codelengths[n++] = 0;
+ }
+ }
+
+ code = lha_prefix_code_from_lengths(codelengths, num, 16, true);
+ free(codelengths);
+ return code;
+ }
+}
+
+/*
+ * Parse the literal Huffman code from the bitstream (uses metacode).
+ */
+static lha_prefix_code *parse_literal_code(lha_bitio *bitio)
+{
+ lha_prefix_code *metacode = parse_code(bitio, 5, 3);
+ lha_prefix_code *code;
+ int num, n;
+ int *codelengths;
+
+ if(!metacode) return NULL;
+
+ num = (int)lha_bitio_next_bits(bitio, 9);
+
+ if(num == 0)
+ {
+ int val = (int)lha_bitio_next_bits(bitio, 9);
+
+ lha_prefix_code_free(metacode);
+ code = lha_prefix_code_new();
+
+ if(!code) return NULL;
+
+ lha_prefix_code_add(code, val, 0, 0);
+ return code;
+ }
+
+ codelengths = (int *)calloc((size_t)num, sizeof(int));
+
+ if(!codelengths)
+ {
+ lha_prefix_code_free(metacode);
+ return NULL;
+ }
+
+ n = 0;
+
+ while(n < num)
+ {
+ int c = lha_prefix_code_decode(bitio, metacode);
+
+ if(c < 0) break;
+
+ if(c <= 2)
+ {
+ int zeros = 0, i;
+
+ switch(c)
+ {
+ case 0: zeros = 1; break;
+ case 1: zeros = (int)lha_bitio_next_bits(bitio, 4) + 3; break;
+ case 2: zeros = (int)lha_bitio_next_bits(bitio, 9) + 20; break;
+ }
+
+ if(n + zeros > num) zeros = num - n;
+
+ for(i = 0; i < zeros; i++) codelengths[n++] = 0;
+ }
+ else
+ {
+ codelengths[n++] = c - 2;
+ }
+ }
+
+ lha_prefix_code_free(metacode);
+ code = lha_prefix_code_from_lengths(codelengths, num, 16, true);
+ free(codelengths);
+ return code;
+}
+
+/*
+ * Internal decompression for LH4/5/6/7 with parameterized window bits.
+ */
+static int lh_static_decompress(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len,
+ int window_bits)
+{
+ lha_bitio bitio;
+ lha_lzss lzss;
+ lha_prefix_code *literalcode = NULL;
+ lha_prefix_code *distancecode = NULL;
+ int blocksize = 0;
+ int blockpos = 0;
+ size_t expected;
+ int dist_width;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ expected = *out_len;
+ dist_width = window_bits < 15 ? 4 : 5;
+
+ if(!lha_lzss_init(&lzss, (size_t)1 << window_bits, out_buf, expected)) return -1;
+
+ lha_bitio_init(&bitio, in_buf, in_len);
+
+ while(lzss.out_pos < expected && !lha_bitio_at_eof(&bitio))
+ {
+ if(blockpos >= blocksize)
+ {
+ /* New block */
+ blocksize = (int)lha_bitio_next_bits(&bitio, 16);
+ blockpos = 0;
+
+ lha_prefix_code_free(literalcode);
+ lha_prefix_code_free(distancecode);
+
+ literalcode = parse_literal_code(&bitio);
+ distancecode = parse_code(&bitio, dist_width, -1);
+
+ if(!literalcode || !distancecode)
+ {
+ lha_prefix_code_free(literalcode);
+ lha_prefix_code_free(distancecode);
+ lha_lzss_cleanup(&lzss);
+ return -1;
+ }
+ }
+
+ blockpos++;
+
+ {
+ int lit = lha_prefix_code_decode(&bitio, literalcode);
+
+ if(lit < 0) break;
+
+ if(lit < 0x100)
+ {
+ lha_lzss_emit_literal(&lzss, (uint8_t)lit);
+ }
+ else
+ {
+ int length = lit - 0x100 + 3;
+ int bit = lha_prefix_code_decode(&bitio, distancecode);
+ int offset;
+
+ if(bit < 0) break;
+
+ if(bit == 0)
+ offset = 1;
+ else if(bit == 1)
+ offset = 2;
+ else
+ offset = (1 << (bit - 1)) + (int)lha_bitio_next_bits(&bitio, bit - 1) + 1;
+
+ lha_lzss_emit_match(&lzss, offset, length);
+ }
+ }
+ }
+
+ *out_len = lzss.out_pos;
+ lha_prefix_code_free(literalcode);
+ lha_prefix_code_free(distancecode);
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh4(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ return lh_static_decompress(in_buf, in_len, out_buf, out_len, 12);
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ return lh_static_decompress(in_buf, in_len, out_buf, out_len, 13);
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh6(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ return lh_static_decompress(in_buf, in_len, out_buf, out_len, 15);
+}
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh7(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len)
+{
+ return lh_static_decompress(in_buf, in_len, out_buf, out_len, 16);
+}
diff --git a/lha/lh_static.h b/lha/lh_static.h
new file mode 100644
index 0000000..ac8d727
--- /dev/null
+++ b/lha/lh_static.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_LH_STATIC_H_
+#define AARU_COMPRESSION_NATIVE__LHA_LH_STATIC_H_
+
+#include
+#include
+#include "../library.h"
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh4(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh6(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+AARU_EXPORT int AARU_CALL lha_decompress_lh7(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_LH_STATIC_H_ */
diff --git a/lha/lzss.c b/lha/lzss.c
new file mode 100644
index 0000000..5023bc9
--- /dev/null
+++ b/lha/lzss.c
@@ -0,0 +1,48 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "lzss.h"
+
+#include
+#include
+
+bool lha_lzss_init(lha_lzss *self, size_t window_size, uint8_t *out_buf, size_t out_size)
+{
+ self->window = (uint8_t *)malloc(window_size);
+
+ if(!self->window) return false;
+
+ self->mask = window_size - 1; /* Assume power-of-two */
+ self->position = 0;
+ self->out_buf = out_buf;
+ self->out_pos = 0;
+ self->out_size = out_size;
+
+ memset(self->window, 0, window_size);
+
+ return true;
+}
+
+void lha_lzss_cleanup(lha_lzss *self)
+{
+ if(self->window)
+ {
+ free(self->window);
+ self->window = NULL;
+ }
+}
diff --git a/lha/lzss.h b/lha/lzss.h
new file mode 100644
index 0000000..0161cf5
--- /dev/null
+++ b/lha/lzss.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_LZSS_H_
+#define AARU_COMPRESSION_NATIVE__LHA_LZSS_H_
+
+#include
+#include
+#include
+
+typedef struct lha_lzss
+{
+ uint8_t *window;
+ size_t mask;
+ int64_t position;
+ uint8_t *out_buf;
+ size_t out_pos;
+ size_t out_size;
+} lha_lzss;
+
+bool lha_lzss_init(lha_lzss *self, size_t window_size, uint8_t *out_buf, size_t out_size);
+void lha_lzss_cleanup(lha_lzss *self);
+
+static inline void lha_lzss_emit_literal(lha_lzss *self, uint8_t literal)
+{
+ self->window[self->position & self->mask] = literal;
+ self->position++;
+
+ if(self->out_pos < self->out_size) self->out_buf[self->out_pos++] = literal;
+}
+
+static inline void lha_lzss_emit_match(lha_lzss *self, int offset, int length)
+{
+ size_t windowoffs = (size_t)(self->position & self->mask);
+ int i;
+
+ for(i = 0; i < length; i++)
+ {
+ uint8_t byte = self->window[(windowoffs + i - offset) & self->mask];
+ self->window[(windowoffs + i) & self->mask] = byte;
+
+ if(self->out_pos < self->out_size) self->out_buf[self->out_pos++] = byte;
+ }
+
+ self->position += length;
+}
+
+static inline uint8_t lha_lzss_get_byte(lha_lzss *self, int64_t pos)
+{
+ return self->window[pos & self->mask];
+}
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_LZSS_H_ */
diff --git a/lha/pmarc1.c b/lha/pmarc1.c
new file mode 100644
index 0000000..8ca352c
--- /dev/null
+++ b/lha/pmarc1.c
@@ -0,0 +1,387 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * Based on code by Simon Howard, ISC license:
+ * Copyright (c) 2011, 2012, Simon Howard
+ * Permission to use, copy, modify, and/or distribute this software
+ * for any purpose with or without fee is hereby granted, provided
+ * that the above copyright notice and this permission notice appear
+ * in all copies.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include "pmarc1.h"
+#include "bitio.h"
+#include "lzss.h"
+
+#include
+#include
+
+#define MAX_BYTE_BLOCK_LEN 216
+
+typedef struct
+{
+ uint8_t prev;
+ uint8_t next;
+} pma1_history_node;
+
+typedef struct
+{
+ pma1_history_node history[256];
+ uint8_t history_head;
+} pma1_history_list;
+
+typedef struct
+{
+ unsigned int offset;
+ unsigned int bits;
+} variable_length_table;
+
+/* --- History list --- */
+
+static void init_history_list(pma1_history_list *list)
+{
+ int i;
+
+ for(i = 0; i < 256; i++)
+ {
+ list->history[i].prev = (uint8_t)(i + 1);
+ list->history[i].next = (uint8_t)(i - 1);
+ }
+
+ list->history_head = 0x20;
+
+ list->history[0x7f].prev = 0x00;
+ list->history[0x00].next = 0x7f;
+ list->history[0x1f].prev = 0xa0;
+ list->history[0xa0].next = 0x1f;
+ list->history[0xdf].prev = 0x80;
+ list->history[0x80].next = 0xdf;
+ list->history[0x9f].prev = 0xe0;
+ list->history[0xe0].next = 0x9f;
+ list->history[0xff].prev = 0x20;
+ list->history[0x20].next = 0xff;
+}
+
+static uint8_t find_in_history_list(pma1_history_list *list, uint8_t count)
+{
+ uint8_t code = list->history_head;
+ int i;
+
+ if(count < 128)
+ {
+ for(i = 0; i < count; i++) code = list->history[code].prev;
+ }
+ else
+ {
+ for(i = 0; i < 256 - count; i++) code = list->history[code].next;
+ }
+
+ return code;
+}
+
+static void update_history_list(pma1_history_list *list, uint8_t b)
+{
+ pma1_history_node *node, *old_head;
+
+ if(list->history_head == b) return;
+
+ node = &list->history[b];
+ old_head = &list->history[list->history_head];
+
+ /* Unhook from current position */
+ list->history[node->next].prev = node->prev;
+ list->history[node->prev].next = node->next;
+
+ /* Hook between old head and its next */
+ node->prev = list->history_head;
+ node->next = old_head->next;
+ list->history[old_head->next].prev = b;
+ old_head->next = b;
+
+ list->history_head = b;
+}
+
+/* --- Variable length tables --- */
+
+static const variable_length_table copy_ranges[] = {
+ {0, 6}, /* 0: 0 + (1<<6) = 64 */
+ {64, 8}, /* 1: 64 + (1<<8) = 320 */
+ {0, 6}, /* 2: 0 + (1<<6) = 64 */
+ {64, 9}, /* 3: 64 + (1<<9) = 576 */
+ {576, 11}, /* 4: 576 + (1<<11) = 2624 */
+ {2624, 13}, /* 5: 2624 + (1<<13) = 10816 */
+ /* Early-stream overrides: */
+ {64, 8}, /* 6: replaces 3 when pos < 320 */
+ {576, 8}, /* 7: replaces 4 when pos < 832 */
+ {576, 9}, /* 8: replaces 4 when pos < 1088 */
+ {576, 10}, /* 9: replaces 4 when pos < 1600 */
+ {2624, 8}, /* 10: replaces 5 when pos < 2880 */
+ {2624, 9}, /* 11: replaces 5 when pos < 3136 */
+ {2624, 10}, /* 12: replaces 5 when pos < 3648 */
+ {2624, 11}, /* 13: replaces 5 when pos < 4672 */
+ {2624, 12}, /* 14: replaces 5 when pos < 6720 */
+};
+
+static const variable_length_table byte_ranges[] = {
+ {0, 4}, /* 0 + (1<<4) = 16 */
+ {16, 4}, /* 16 + (1<<4) = 32 */
+ {32, 5}, /* 32 + (1<<5) = 64 */
+ {64, 6}, /* 64 + (1<<6) = 128 */
+ {128, 6}, /* 128 + (1<<6) = 192 */
+ {192, 6}, /* 192 + (1<<6) = 256 */
+};
+
+static const uint8_t byte_decode_trees[][5] = {
+ {0x12, 0x2d, 0xef, 0x1c, 0xab}, {0x12, 0x23, 0xde, 0xab, 0xcf}, {0x12, 0x2c, 0xd2, 0xab, 0xef},
+ {0x12, 0xa2, 0xd2, 0xbc, 0xef}, {0x12, 0xa2, 0xc2, 0xbd, 0xef}, {0x12, 0xa2, 0xcd, 0xb1, 0xef},
+ {0x12, 0xab, 0x12, 0xcd, 0xef}, {0x12, 0xab, 0x1d, 0xc1, 0xef}, {0x12, 0xab, 0xc1, 0xd1, 0xef},
+ {0xa1, 0x12, 0x2c, 0xde, 0xbf}, {0xa1, 0x1d, 0x1c, 0xb1, 0xef}, {0xa1, 0x12, 0x2d, 0xef, 0xbc},
+ {0xa1, 0x12, 0xb2, 0xde, 0xcf}, {0xa1, 0x12, 0xbc, 0xd1, 0xef}, {0xa1, 0x1c, 0xb1, 0xd1, 0xef},
+ {0xa1, 0xb1, 0x12, 0xcd, 0xef}, {0xa1, 0xb1, 0xc1, 0xd1, 0xef}, {0x12, 0x1c, 0xde, 0xab, 0x00},
+ {0x12, 0xa2, 0xcd, 0xbe, 0x00}, {0x12, 0xab, 0xc1, 0xde, 0x00}, {0xa1, 0x1d, 0x1c, 0xbe, 0x00},
+ {0xa1, 0x12, 0xbc, 0xde, 0x00}, {0xa1, 0x1c, 0xb1, 0xde, 0x00}, {0xa1, 0xb1, 0xc1, 0xde, 0x00},
+ {0x1d, 0x1c, 0xab, 0x00, 0x00}, {0x1c, 0xa1, 0xbd, 0x00, 0x00}, {0x12, 0xab, 0xcd, 0x00, 0x00},
+ {0xa1, 0x1c, 0xbd, 0x00, 0x00}, {0xa1, 0xb1, 0xcd, 0x00, 0x00}, {0xa1, 0xbc, 0x00, 0x00, 0x00},
+ {0xab, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00, 0x00},
+};
+
+/* --- Decode helper functions --- */
+
+static int decode_variable_length(lha_bitio *input, const variable_length_table *table, unsigned int header)
+{
+ int value = (int)lha_bitio_next_bits(input, (int)table[header].bits);
+
+ return (int)table[header].offset + value;
+}
+
+static int next_bit_after_threshold(lha_bitio *input, unsigned int pos, unsigned int threshold, int def)
+{
+ if(pos >= threshold) return (int)lha_bitio_next_bit(input);
+
+ return def;
+}
+
+static int read_copy_byte_count(lha_bitio *input)
+{
+ int x = (int)lha_bitio_next_bits(input, 2);
+
+ if(x < 3) return x + 3;
+
+ x = (int)lha_bitio_next_bits(input, 3);
+
+ if(x < 5) return x + 6;
+ else if(x == 5) return (int)lha_bitio_next_bits(input, 2) + 11;
+ else if(x == 6) return (int)lha_bitio_next_bits(input, 3) + 15;
+
+ /* x == 7 */
+ x = (int)lha_bitio_next_bits(input, 6);
+
+ if(x < 62) return x + 23;
+ else if(x == 62) return (int)lha_bitio_next_bits(input, 5) + 85;
+
+ /* x == 63 */
+ return (int)lha_bitio_next_bits(input, 7) + 117;
+}
+
+static int read_copy_type_range(lha_bitio *input, unsigned int pos)
+{
+ int x = (int)lha_bitio_next_bit(input);
+
+ if(x == 0)
+ {
+ x = next_bit_after_threshold(input, pos, 576, 0);
+
+ if(x != 0) return 4;
+
+ return next_bit_after_threshold(input, pos, 64, 0);
+ }
+ else
+ {
+ x = next_bit_after_threshold(input, pos, 64, 1);
+
+ if(x == 0) return 3;
+
+ x = next_bit_after_threshold(input, pos, 2624, 1);
+
+ if(x != 0) return 2;
+
+ return 5;
+ }
+}
+
+static int read_byte_decode_index(lha_bitio *input, const uint8_t *tree)
+{
+ const uint8_t *ptr = tree;
+
+ if(ptr[0] == 0) return 0;
+
+ for(;;)
+ {
+ int bit = (int)lha_bitio_next_bit(input);
+ unsigned int child;
+
+ if(bit == 0)
+ child = (*ptr >> 4) & 0x0f;
+ else
+ child = *ptr & 0x0f;
+
+ if(child >= 10) return (int)(child - 10);
+
+ ptr += child;
+ }
+}
+
+static int read_byte(lha_bitio *input, const uint8_t *tree, pma1_history_list *history)
+{
+ int index = read_byte_decode_index(input, tree);
+ int count = decode_variable_length(input, byte_ranges, (unsigned int)index);
+
+ return find_in_history_list(history, (uint8_t)count);
+}
+
+static int read_byte_block_count(lha_bitio *input)
+{
+ int x = (int)lha_bitio_next_bits(input, 2);
+
+ if(x < 3) return x + 1;
+
+ x = (int)lha_bitio_next_bits(input, 3);
+
+ if(x < 7) return x + 4;
+
+ x = (int)lha_bitio_next_bits(input, 4);
+
+ if(x < 14) return x + 11;
+ else if(x == 14) return (int)lha_bitio_next_bits(input, 6) + 25;
+
+ /* x == 15 */
+ return (int)lha_bitio_next_bits(input, 7) + 89;
+}
+
+/* --- Main decompression --- */
+
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf,
+ size_t *out_len)
+{
+ lha_bitio bitio;
+ lha_lzss lzss;
+ pma1_history_list history;
+ const uint8_t *byte_tree;
+ size_t expected;
+ int bytes_left = 0;
+ bool next_is_match = false;
+
+ if(!in_buf || !out_buf || !out_len || in_len == 0) return -1;
+
+ expected = *out_len;
+
+ if(!lha_lzss_init(&lzss, 16384, out_buf, expected)) return -1;
+
+ lha_bitio_init(&bitio, in_buf, in_len);
+ init_history_list(&history);
+
+ /* Read 5-bit header selecting the byte decode tree */
+ {
+ int index = (int)lha_bitio_next_bits(&bitio, 5);
+ byte_tree = byte_decode_trees[index];
+ }
+
+ while(lzss.out_pos < expected && !lha_bitio_at_eof(&bitio))
+ {
+ if(bytes_left > 0)
+ {
+ /* Emit a byte from the history-encoded stream */
+ uint8_t b = (uint8_t)read_byte(&bitio, byte_tree, &history);
+ lha_lzss_emit_literal(&lzss, b);
+ update_history_list(&history, b);
+ bytes_left--;
+ }
+ else if(next_is_match || lha_bitio_next_bit(&bitio) == 0)
+ {
+ /* Match/copy */
+ int range_index, count, history_distance, offset;
+
+ next_is_match = false;
+ range_index = read_copy_type_range(&bitio, (unsigned int)lzss.position);
+
+ if(range_index < 2)
+ count = 2;
+ else
+ count = read_copy_byte_count(&bitio);
+
+ /* Apply early-stream overrides */
+ if(range_index == 3)
+ {
+ if(lzss.position < 320) range_index = 6;
+ }
+ else if(range_index == 4)
+ {
+ if(lzss.position < 832)
+ range_index = 7;
+ else if(lzss.position < 1088)
+ range_index = 8;
+ else if(lzss.position < 1600)
+ range_index = 9;
+ }
+ else if(range_index == 5)
+ {
+ if(lzss.position < 2880)
+ range_index = 10;
+ else if(lzss.position < 3136)
+ range_index = 11;
+ else if(lzss.position < 3648)
+ range_index = 12;
+ else if(lzss.position < 4672)
+ range_index = 13;
+ else if(lzss.position < 6720)
+ range_index = 14;
+ }
+
+ history_distance = decode_variable_length(&bitio, copy_ranges, (unsigned int)range_index);
+ offset = history_distance + 1;
+
+ lha_lzss_emit_match(&lzss, offset, count);
+
+ /* Update history for all emitted match bytes */
+ {
+ size_t match_start = lzss.out_pos - (size_t)count;
+ int i;
+
+ for(i = 0; i < count; i++) update_history_list(&history, out_buf[match_start + i]);
+ }
+ }
+ else
+ {
+ /* Byte block: read count, then emit that many literal bytes */
+ bytes_left = read_byte_block_count(&bitio);
+
+ if(bytes_left < MAX_BYTE_BLOCK_LEN) next_is_match = true;
+
+ /* Process first byte immediately */
+ {
+ uint8_t b = (uint8_t)read_byte(&bitio, byte_tree, &history);
+ lha_lzss_emit_literal(&lzss, b);
+ update_history_list(&history, b);
+ bytes_left--;
+ }
+ }
+ }
+
+ *out_len = lzss.out_pos;
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
diff --git a/lha/pmarc1.h b/lha/pmarc1.h
new file mode 100644
index 0000000..dca902e
--- /dev/null
+++ b/lha/pmarc1.h
@@ -0,0 +1,29 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__LHA_PMARC1_H_
+#define AARU_COMPRESSION_NATIVE__LHA_PMARC1_H_
+
+#include
+#include
+#include "../library.h"
+
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf,
+ size_t *out_len);
+
+#endif /* AARU_COMPRESSION_NATIVE__LHA_PMARC1_H_ */
diff --git a/library.h b/library.h
index eed194a..caeead9 100644
--- a/library.h
+++ b/library.h
@@ -201,4 +201,37 @@ AARU_EXPORT int AARU_CALL ha_asc_decompress(const unsigned char *in_buf, size_t
AARU_EXPORT int AARU_CALL ha_hsc_decompress(const unsigned char *in_buf, size_t in_len, unsigned char *out_buf, size_t *out_len);
+// LHA -lh1- (Dynamic Huffman, 4KB window)
+AARU_EXPORT int AARU_CALL lha_decompress_lh1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh2- (Dynamic Huffman, legacy)
+AARU_EXPORT int AARU_CALL lha_decompress_lh2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh3- (Static Huffman, legacy)
+AARU_EXPORT int AARU_CALL lha_decompress_lh3(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh4- (Block Huffman, 4KB window)
+AARU_EXPORT int AARU_CALL lha_decompress_lh4(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh5- (Block Huffman, 8KB window)
+AARU_EXPORT int AARU_CALL lha_decompress_lh5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh6- (Block Huffman, 32KB window)
+AARU_EXPORT int AARU_CALL lha_decompress_lh6(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LHA -lh7- (Block Huffman, 64KB window)
+AARU_EXPORT int AARU_CALL lha_decompress_lh7(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LArc -lzs- (LZSS, 2KB window)
+AARU_EXPORT int AARU_CALL larc_decompress_lzs(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// LArc -lz5- (Flag-byte LZSS, 4KB window)
+AARU_EXPORT int AARU_CALL larc_decompress_lz5(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// PMarc -pm1- (Adaptive history + Huffman)
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm1(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
+// PMarc -pm2- (Dynamic trees + history, legacy)
+AARU_EXPORT int AARU_CALL pmarc_decompress_pm2(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len);
+
#endif // AARU_COMPRESSION_NATIVE_LIBRARY_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index dca949b..6919daf 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -78,9 +78,22 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/ha_hsc.bin
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/xz.xz
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/larc_lz5.lzs
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/lha_lh6.lzh
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/lharc_lh1.lzh
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/pmarc_pm2.pma
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
# 'Google_Tests_run' is the target name
# 'test1.cpp tests2.cpp' are source files with tests
add_executable(tests_run apple_rle.cpp crc32.c crc32.h adc.cpp bzip2.cpp lzip.cpp lzfse.cpp zstd.cpp lzma.cpp flac.cpp lz4.cpp
zoo/lzd.cpp arc/pack.cpp lh5.cpp arc/squeeze.cpp arc/crunch.cpp arc/squash.cpp pak/crush.cpp
- pak/distill.cpp ha.cpp xz.cpp)
+ pak/distill.cpp ha.cpp xz.cpp
+ lha/lh_static.cpp lha/lh1.cpp lha/larc.cpp lha/lh_old.cpp)
target_link_libraries(tests_run gtest gtest_main "Aaru.Compression.Native")
diff --git a/tests/data/larc_lz5.lzs b/tests/data/larc_lz5.lzs
new file mode 100644
index 0000000..ba1f3a7
Binary files /dev/null and b/tests/data/larc_lz5.lzs differ
diff --git a/tests/data/lha_lh6.lzh b/tests/data/lha_lh6.lzh
new file mode 100644
index 0000000..535d3a1
Binary files /dev/null and b/tests/data/lha_lh6.lzh differ
diff --git a/tests/data/lharc_lh1.lzh b/tests/data/lharc_lh1.lzh
new file mode 100755
index 0000000..82e4700
Binary files /dev/null and b/tests/data/lharc_lh1.lzh differ
diff --git a/tests/data/pmarc_pm2.pma b/tests/data/pmarc_pm2.pma
new file mode 100755
index 0000000..9cd15f4
Binary files /dev/null and b/tests/data/pmarc_pm2.pma differ
diff --git a/tests/lha/larc.cpp b/tests/lha/larc.cpp
new file mode 100644
index 0000000..f4dc6f7
--- /dev/null
+++ b/tests/lha/larc.cpp
@@ -0,0 +1,74 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include
+#include
+#include
+
+#include "../../library.h"
+#include "../../lha/larc.h"
+#include "../crc32.h"
+#include "gtest/gtest.h"
+#include "lha_helpers.h"
+
+/* alice29.txt decompressed CRC32 */
+#define EXPECTED_CRC32 0x66007dba
+#define EXPECTED_SIZE 152089
+
+static uint8_t *lz5_payload;
+static lha_test_info lz5_info;
+
+class lz5Fixture : public ::testing::Test
+{
+protected:
+ void SetUp()
+ {
+ char path[PATH_MAX];
+ char filename[PATH_MAX];
+
+ getcwd(path, PATH_MAX);
+ snprintf(filename, PATH_MAX, "%s/data/larc_lz5.lzs", path);
+
+ lz5_payload = load_lha_payload(filename, &lz5_info);
+ }
+
+ void TearDown()
+ {
+ if(lz5_payload) free(lz5_payload);
+ }
+};
+
+TEST_F(lz5Fixture, lz5_decompress)
+{
+ ASSERT_NE(lz5_payload, nullptr);
+ EXPECT_EQ(lz5_info.uncompressed_size, (uint32_t)EXPECTED_SIZE);
+
+ size_t out_len = EXPECTED_SIZE;
+ uint8_t *out_buf = (uint8_t *)malloc(out_len);
+
+ int err = larc_decompress_lz5(lz5_payload, lz5_info.payload_size, out_buf, &out_len);
+
+ EXPECT_EQ(err, 0);
+ EXPECT_EQ(out_len, (size_t)EXPECTED_SIZE);
+
+ uint32_t crc = crc32_data(out_buf, (uint32_t)out_len);
+
+ free(out_buf);
+
+ EXPECT_EQ(crc, (uint32_t)EXPECTED_CRC32);
+}
diff --git a/tests/lha/lh1.cpp b/tests/lha/lh1.cpp
new file mode 100644
index 0000000..814416c
--- /dev/null
+++ b/tests/lha/lh1.cpp
@@ -0,0 +1,74 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include
+#include
+#include
+
+#include "../../library.h"
+#include "../../lha/lh1.h"
+#include "../crc32.h"
+#include "gtest/gtest.h"
+#include "lha_helpers.h"
+
+/* alice29.txt decompressed CRC32 */
+#define EXPECTED_CRC32 0x66007dba
+#define EXPECTED_SIZE 152089
+
+static uint8_t *lh1_payload;
+static lha_test_info lh1_info;
+
+class lh1Fixture : public ::testing::Test
+{
+protected:
+ void SetUp()
+ {
+ char path[PATH_MAX];
+ char filename[PATH_MAX];
+
+ getcwd(path, PATH_MAX);
+ snprintf(filename, PATH_MAX, "%s/data/lharc_lh1.lzh", path);
+
+ lh1_payload = load_lha_payload(filename, &lh1_info);
+ }
+
+ void TearDown()
+ {
+ if(lh1_payload) free(lh1_payload);
+ }
+};
+
+TEST_F(lh1Fixture, lh1_decompress)
+{
+ ASSERT_NE(lh1_payload, nullptr);
+ EXPECT_EQ(lh1_info.uncompressed_size, (uint32_t)EXPECTED_SIZE);
+
+ size_t out_len = EXPECTED_SIZE;
+ uint8_t *out_buf = (uint8_t *)malloc(out_len);
+
+ int err = lha_decompress_lh1(lh1_payload, lh1_info.payload_size, out_buf, &out_len);
+
+ EXPECT_EQ(err, 0);
+ EXPECT_EQ(out_len, (size_t)EXPECTED_SIZE);
+
+ uint32_t crc = crc32_data(out_buf, (uint32_t)out_len);
+
+ free(out_buf);
+
+ EXPECT_EQ(crc, (uint32_t)EXPECTED_CRC32);
+}
diff --git a/tests/lha/lh_old.cpp b/tests/lha/lh_old.cpp
new file mode 100644
index 0000000..6dd2006
--- /dev/null
+++ b/tests/lha/lh_old.cpp
@@ -0,0 +1,75 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include
+#include
+#include
+
+#include "../../library.h"
+#include "../../lha/lh_old.h"
+#include "../crc32.h"
+#include "gtest/gtest.h"
+#include "lha_helpers.h"
+
+/* PMarc -pm2- decompresses to 152192 bytes */
+#define PM2_EXPECTED_SIZE 152192
+#define PM2_EXPECTED_CRC32 0x1bbf031e
+
+static uint8_t *pm2_payload;
+static lha_test_info pm2_info;
+
+class pm2Fixture : public ::testing::Test
+{
+protected:
+ void SetUp()
+ {
+ char path[PATH_MAX];
+ char filename[PATH_MAX];
+
+ getcwd(path, PATH_MAX);
+ snprintf(filename, PATH_MAX, "%s/data/pmarc_pm2.pma", path);
+
+ pm2_payload = load_lha_payload(filename, &pm2_info);
+ }
+
+ void TearDown()
+ {
+ if(pm2_payload) free(pm2_payload);
+ }
+};
+
+TEST_F(pm2Fixture, pm2_decompress)
+{
+ ASSERT_NE(pm2_payload, nullptr);
+ EXPECT_EQ(pm2_info.uncompressed_size, (uint32_t)PM2_EXPECTED_SIZE);
+
+ size_t out_len = PM2_EXPECTED_SIZE;
+ uint8_t *out_buf = (uint8_t *)malloc(out_len);
+
+ int err = pmarc_decompress_pm2(pm2_payload, pm2_info.payload_size, out_buf, &out_len);
+
+ EXPECT_EQ(err, 0);
+ EXPECT_EQ(out_len, (size_t)PM2_EXPECTED_SIZE);
+
+ uint32_t crc = crc32_data(out_buf, (uint32_t)out_len);
+
+ free(out_buf);
+
+ /* Validate CRC32 of decompressed data */
+ EXPECT_EQ(crc, (uint32_t)PM2_EXPECTED_CRC32);
+}
diff --git a/tests/lha/lh_static.cpp b/tests/lha/lh_static.cpp
new file mode 100644
index 0000000..bb8e5bb
--- /dev/null
+++ b/tests/lha/lh_static.cpp
@@ -0,0 +1,74 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include
+#include
+#include
+
+#include "../../library.h"
+#include "../../lha/lh_static.h"
+#include "../crc32.h"
+#include "gtest/gtest.h"
+#include "lha_helpers.h"
+
+/* alice29.txt decompressed CRC32 (same as zoo/lh5 test) */
+#define EXPECTED_CRC32 0x66007dba
+#define EXPECTED_SIZE 152089
+
+static uint8_t *lh6_payload;
+static lha_test_info lh6_info;
+
+class lh6Fixture : public ::testing::Test
+{
+protected:
+ void SetUp()
+ {
+ char path[PATH_MAX];
+ char filename[PATH_MAX];
+
+ getcwd(path, PATH_MAX);
+ snprintf(filename, PATH_MAX, "%s/data/lha_lh6.lzh", path);
+
+ lh6_payload = load_lha_payload(filename, &lh6_info);
+ }
+
+ void TearDown()
+ {
+ if(lh6_payload) free(lh6_payload);
+ }
+};
+
+TEST_F(lh6Fixture, lh6_decompress)
+{
+ ASSERT_NE(lh6_payload, nullptr);
+ EXPECT_EQ(lh6_info.uncompressed_size, (uint32_t)EXPECTED_SIZE);
+
+ size_t out_len = EXPECTED_SIZE;
+ uint8_t *out_buf = (uint8_t *)malloc(out_len);
+
+ int err = lha_decompress_lh6(lh6_payload, lh6_info.payload_size, out_buf, &out_len);
+
+ EXPECT_EQ(err, 0);
+ EXPECT_EQ(out_len, (size_t)EXPECTED_SIZE);
+
+ uint32_t crc = crc32_data(out_buf, (uint32_t)out_len);
+
+ free(out_buf);
+
+ EXPECT_EQ(crc, (uint32_t)EXPECTED_CRC32);
+}
diff --git a/tests/lha/lha_helpers.h b/tests/lha/lha_helpers.h
new file mode 100644
index 0000000..653fc26
--- /dev/null
+++ b/tests/lha/lha_helpers.h
@@ -0,0 +1,122 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#ifndef AARU_COMPRESSION_NATIVE__TESTS_LHA_HELPERS_H_
+#define AARU_COMPRESSION_NATIVE__TESTS_LHA_HELPERS_H_
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/*
+ * Minimal LHA archive header parser for tests.
+ * Extracts compressed payload from an LHA/LArc/PMarc archive file.
+ * Supports header levels 0, 1, and 2.
+ */
+struct lha_test_info
+{
+ char method[6]; /* e.g., "-lh6-" */
+ uint32_t compressed_size;
+ uint32_t uncompressed_size;
+ uint8_t level;
+ size_t payload_offset; /* offset of compressed data in the file */
+ size_t payload_size; /* actual compressed data size */
+};
+
+static int parse_lha_header(const uint8_t *data, size_t data_len, struct lha_test_info *info)
+{
+ if(data_len < 22) return -1;
+
+ uint8_t header_size_byte = data[0];
+ uint32_t compressed_size = (uint32_t)data[7] | ((uint32_t)data[8] << 8) | ((uint32_t)data[9] << 16) |
+ ((uint32_t)data[10] << 24);
+ uint32_t uncompressed_size = (uint32_t)data[11] | ((uint32_t)data[12] << 8) | ((uint32_t)data[13] << 16) |
+ ((uint32_t)data[14] << 24);
+ uint8_t level = data[20];
+
+ memcpy(info->method, &data[2], 5);
+ info->method[5] = '\0';
+ info->compressed_size = compressed_size;
+ info->uncompressed_size = uncompressed_size;
+ info->level = level;
+
+ if(level == 0 || level == 1)
+ {
+ size_t base_header = (size_t)header_size_byte + 2;
+ info->payload_offset = base_header;
+ info->payload_size = compressed_size;
+ }
+ else if(level == 2)
+ {
+ uint16_t header_size = (uint16_t)data[0] | ((uint16_t)data[1] << 8);
+ info->payload_offset = header_size;
+ info->payload_size = compressed_size;
+ }
+ else
+ {
+ return -1;
+ }
+
+ return 0;
+}
+
+static uint8_t *load_lha_payload(const char *filename, struct lha_test_info *info)
+{
+ FILE *file;
+ size_t file_size;
+ uint8_t *file_data;
+ uint8_t *payload;
+
+ file = fopen(filename, "rb");
+ if(!file) return NULL;
+
+ fseek(file, 0, SEEK_END);
+ file_size = (size_t)ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ file_data = (uint8_t *)malloc(file_size);
+ if(!file_data) { fclose(file); return NULL; }
+
+ fread(file_data, 1, file_size, file);
+ fclose(file);
+
+ if(parse_lha_header(file_data, file_size, info) != 0)
+ {
+ free(file_data);
+ return NULL;
+ }
+
+ if(info->payload_offset + info->payload_size > file_size)
+ {
+ free(file_data);
+ return NULL;
+ }
+
+ payload = (uint8_t *)malloc(info->payload_size);
+ if(!payload) { free(file_data); return NULL; }
+
+ memcpy(payload, file_data + info->payload_offset, info->payload_size);
+ free(file_data);
+
+ return payload;
+}
+
+#endif /* AARU_COMPRESSION_NATIVE__TESTS_LHA_HELPERS_H_ */