diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3f31f5c..513c36a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,7 +131,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_subdirectory(3rdparty)
-add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h adc.c adc.h lzip.c flac.c flac.h
+add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h apple_lzh.c apple_lzh.h adc.c adc.h lzip.c flac.c flac.h
zoo/lzd.c zoo/lzd.h zoo/lzh.c zoo/decode.c zoo/huf.c zoo/io.c zoo/lh5.c zoo/lh5.h zoo/lzh.h zoo/ar.h zoo/maketbl.c
arc/pack.c arc/squeeze.c arc/crunch.c arc/lzw.c
pak/crush.c pak/distill.c pak/bitstream.c pak/bitstream.h pak/lzw.c pak/lzw.h pak/prefixcode.c
diff --git a/apple_lzh.c b/apple_lzh.c
new file mode 100644
index 0000000..b88cfd0
--- /dev/null
+++ b/apple_lzh.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 .
+ */
+
+/*
+ * Apple LZH decompression — used by DART disk image format "best" compression.
+ *
+ * This is identical to LHA -lh1- (adaptive Huffman + LZSS, 4KB window) except
+ * the LZSS window is pre-filled with zeros in the last 110 bytes rather than
+ * spaces (0x20). The standard LH1 format uses spaces as specified by Okumura's
+ * original LZHUF.C; Apple's DART tool uses zeros instead.
+ *
+ * Reference: XADLZHDynamicHandle from The Unarchiver with prefill:NO
+ */
+
+#include "apple_lzh.h"
+#include "lha/bitio.h"
+#include "lha/huffman.h"
+#include "lha/lzss.h"
+
+#include
+
+#define APPLE_LZH_NUM_LEAVES 314
+#define APPLE_LZH_NUM_NODES (APPLE_LZH_NUM_LEAVES * 2 - 1)
+
+typedef struct apple_lzh_node
+{
+ struct apple_lzh_node *parent;
+ struct apple_lzh_node *leftchild;
+ struct apple_lzh_node *rightchild;
+ int index;
+ int freq;
+ int value;
+} apple_lzh_node;
+
+typedef struct
+{
+ apple_lzh_node storage[APPLE_LZH_NUM_NODES];
+ apple_lzh_node *nodes[APPLE_LZH_NUM_NODES];
+} apple_lzh_tree;
+
+static void apple_lzh_init_tree(apple_lzh_tree *tree)
+{
+ int i;
+
+ memset(tree->storage, 0, sizeof(tree->storage));
+
+ for(i = 0; i < APPLE_LZH_NUM_NODES; i++) tree->nodes[i] = &tree->storage[i];
+
+ for(i = 0; i < APPLE_LZH_NUM_LEAVES; i++)
+ {
+ int index = APPLE_LZH_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;
+ }
+
+ for(i = APPLE_LZH_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 apple_lzh_rearrange(apple_lzh_tree *tree, apple_lzh_node *node)
+{
+ int p_index = node->index;
+ int q_index = p_index;
+ apple_lzh_node *q;
+
+ while(q_index > 0 && tree->nodes[q_index - 1]->freq < node->freq) q_index--;
+
+ if(q_index >= p_index) return;
+
+ q = tree->nodes[q_index];
+
+ {
+ apple_lzh_node *new_q_parent = node->parent;
+ apple_lzh_node *new_p_parent = q->parent;
+ int p_is_right = (node->parent && node->parent->rightchild == node);
+ int q_is_right = (q->parent && q->parent->rightchild == q);
+
+ if(node->parent)
+ {
+ if(p_is_right)
+ node->parent->rightchild = q;
+ else
+ node->parent->leftchild = q;
+ }
+
+ if(q->parent)
+ {
+ if(q_is_right)
+ q->parent->rightchild = node;
+ else
+ q->parent->leftchild = node;
+ }
+
+ node->parent = new_p_parent;
+ q->parent = new_q_parent;
+ }
+
+ tree->nodes[p_index] = q;
+ tree->nodes[p_index]->index = p_index;
+ tree->nodes[q_index] = node;
+ tree->nodes[q_index]->index = q_index;
+}
+
+static void apple_lzh_reconstruct(apple_lzh_tree *tree)
+{
+ apple_lzh_node *leaves[APPLE_LZH_NUM_LEAVES];
+ int n = 0;
+ int i;
+ int leaf_index, branch_index, node_index, pair_index;
+
+ for(i = 0; i < APPLE_LZH_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 = APPLE_LZH_NUM_LEAVES - 1;
+ branch_index = APPLE_LZH_NUM_LEAVES - 2;
+ node_index = APPLE_LZH_NUM_NODES - 1;
+ pair_index = APPLE_LZH_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--;
+ }
+
+ {
+ apple_lzh_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 apple_lzh_update(apple_lzh_tree *tree, apple_lzh_node *node)
+{
+ if(tree->nodes[0]->freq == 0x8000) apple_lzh_reconstruct(tree);
+
+ for(;;)
+ {
+ node->freq++;
+
+ if(!node->parent) break;
+
+ apple_lzh_rearrange(tree, node);
+ node = node->parent;
+ }
+}
+
+AARU_EXPORT int AARU_CALL AARU_apple_lzh_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer,
+ size_t src_size)
+{
+ lha_bitio bitio;
+ lha_lzss lzss;
+ apple_lzh_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(!dst_buffer || !dst_size || !src_buffer || src_size == 0) return -1;
+
+ expected = *dst_size;
+
+ if(!lha_lzss_init(&lzss, 4096, dst_buffer, expected)) return -1;
+
+ /* Pre-fill window with pattern identical to 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);
+
+ /* Apple variant: zeros instead of spaces for last 110 bytes */
+ memset(&lzss.window[256 * 13 + 512 + 128 + 18], 0, 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, src_buffer, src_size);
+ apple_lzh_init_tree(&tree);
+
+ while(lzss.out_pos < expected && !lha_bitio_at_eof(&bitio))
+ {
+ apple_lzh_node *node = tree.nodes[0];
+
+ while(node->leftchild || node->rightchild)
+ {
+ if(lha_bitio_next_bit(&bitio))
+ node = node->leftchild;
+ else
+ node = node->rightchild;
+
+ if(!node) break;
+ }
+
+ if(!node) break;
+
+ apple_lzh_update(&tree, node);
+
+ if(node->value < 0x100) { lha_lzss_emit_literal(&lzss, (uint8_t)node->value); }
+ else
+ {
+ 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);
+ }
+ }
+
+ *dst_size = lzss.out_pos;
+ lha_prefix_code_free(distancecode);
+ lha_lzss_cleanup(&lzss);
+ return 0;
+}
diff --git a/apple_lzh.h b/apple_lzh.h
new file mode 100644
index 0000000..fd32b8f
--- /dev/null
+++ b/apple_lzh.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__APPLE_LZH_H_
+#define AARU_COMPRESSION_NATIVE__APPLE_LZH_H_
+
+#include
+#include
+#include "library.h"
+
+AARU_EXPORT int AARU_CALL AARU_apple_lzh_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer,
+ size_t src_size);
+
+#endif /* AARU_COMPRESSION_NATIVE__APPLE_LZH_H_ */
diff --git a/library.h b/library.h
index 86973d5..c9bb2e3 100644
--- a/library.h
+++ b/library.h
@@ -60,6 +60,10 @@ AARU_EXPORT int32_t AARU_CALL AARU_adc_decode_buffer(uint8_t *dst_buffer, int32_
AARU_EXPORT int32_t AARU_CALL AARU_apple_rle_decode_buffer(uint8_t *dst_buffer, int32_t dst_size,
const uint8_t *src_buffer, int32_t src_size);
+// Apple LZH decompression (DART "best" mode: LH1 variant with zero-filled window tail)
+AARU_EXPORT int AARU_CALL AARU_apple_lzh_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer,
+ size_t src_size);
+
AARU_EXPORT size_t AARU_CALL AARU_flac_decode_redbook_buffer(uint8_t *dst_buffer, size_t dst_size,
const uint8_t *src_buffer, size_t src_size);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index eddae02..fa6e371 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -195,6 +195,30 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffitx_deflate.bin
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffitx_blend.bin
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf2dd_hfs_fast.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf2dd_hfs_best.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_hfs_fast.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_hfs_best.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf2dd_mfs_fast.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf2dd_mfs_best.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_mfs_fast.dart.lz
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_mfs_best.dart.lz
+ 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
@@ -208,5 +232,6 @@ add_executable(tests_run apple_rle.cpp crc32.c crc32.h adc.cpp bzip2.cpp lzip.cp
rar/rar.cpp
cpt/cpt.cpp
dd/dd.cpp
- stuffit/stuffit.cpp)
+ stuffit/stuffit.cpp
+ dart/dart.cpp)
target_link_libraries(tests_run gtest gtest_main "Aaru.Compression.Native")
diff --git a/tests/dart/dart.cpp b/tests/dart/dart.cpp
new file mode 100644
index 0000000..9f38c28
--- /dev/null
+++ b/tests/dart/dart.cpp
@@ -0,0 +1,244 @@
+/*
+ * 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
+#include
+#include
+
+#include "../../library.h"
+#include "../crc32.h"
+#include "dart_helpers.h"
+#include "gtest/gtest.h"
+
+typedef struct
+{
+ uint8_t *data;
+ size_t total_size;
+ dart_header_t header;
+ int16_t block_lengths[DART_BLOCK_ARRAY_LEN_HIGH];
+ int num_blocks;
+ uint8_t *raw;
+ size_t raw_size;
+ size_t data_offset;
+} dart_image_t;
+
+static size_t read_file_to_buffer(const char *path, uint8_t **out)
+{
+ FILE *f = fopen(path, "rb");
+ if(!f) return 0;
+ fseek(f, 0, SEEK_END);
+ long sz = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ if(sz <= 0)
+ {
+ fclose(f);
+ return 0;
+ }
+ *out = (uint8_t *)malloc(sz);
+ if(!*out)
+ {
+ fclose(f);
+ return 0;
+ }
+ size_t rd = fread(*out, 1, sz, f);
+ fclose(f);
+ if(rd != (size_t)sz)
+ {
+ free(*out);
+ *out = NULL;
+ return 0;
+ }
+ return (size_t)sz;
+}
+
+static size_t lzip_decompress_file(const char *path, uint8_t **out)
+{
+ uint8_t *lz_buf = NULL;
+ size_t lz_size = read_file_to_buffer(path, &lz_buf);
+ if(lz_size == 0) return 0;
+
+ size_t try_sizes[] = {512 * 1024, 1024 * 1024, 2 * 1024 * 1024};
+ for(int i = 0; i < 3; i++)
+ {
+ *out = (uint8_t *)malloc(try_sizes[i]);
+ int32_t result = AARU_lzip_decode_buffer(*out, (int32_t)try_sizes[i], lz_buf, (int32_t)lz_size);
+ if(result > 0)
+ {
+ free(lz_buf);
+ return (size_t)result;
+ }
+ free(*out);
+ *out = NULL;
+ }
+ free(lz_buf);
+ return 0;
+}
+
+static bool load_dart_image(const char *path, dart_image_t *img)
+{
+ memset(img, 0, sizeof(*img));
+
+ img->raw_size = lzip_decompress_file(path, &img->raw);
+ if(img->raw_size == 0) return false;
+
+ img->data_offset = dart_parse_header(img->raw, img->raw_size, &img->header, img->block_lengths, &img->num_blocks);
+ if(img->data_offset == 0)
+ {
+ free(img->raw);
+ img->raw = NULL;
+ return false;
+ }
+
+ int active_blocks = 0;
+ for(int i = 0; i < img->num_blocks; i++)
+ {
+ if(img->block_lengths[i] != 0) active_blocks++;
+ }
+ img->total_size = active_blocks * DART_BUFFER_SIZE;
+
+ if(img->header.srcCmp == DART_COMPRESS_RLE)
+ {
+ img->data = (uint8_t *)malloc(img->total_size);
+ size_t out_pos = 0;
+ size_t in_pos = img->data_offset;
+
+ for(int i = 0; i < img->num_blocks; i++)
+ {
+ if(img->block_lengths[i] == 0) continue;
+
+ if(img->block_lengths[i] == -1)
+ {
+ if(in_pos + DART_BUFFER_SIZE <= img->raw_size)
+ memcpy(img->data + out_pos, img->raw + in_pos, DART_BUFFER_SIZE);
+ in_pos += DART_BUFFER_SIZE;
+ }
+ else
+ {
+ size_t comp_size = (size_t)img->block_lengths[i] * 2;
+ if(in_pos + comp_size <= img->raw_size)
+ {
+ AARU_apple_rle_decode_buffer(img->data + out_pos, DART_BUFFER_SIZE, img->raw + in_pos,
+ (int32_t)comp_size);
+ }
+ in_pos += comp_size;
+ }
+ out_pos += DART_BUFFER_SIZE;
+ }
+ }
+
+ return true;
+}
+
+static void free_dart_image(dart_image_t *img)
+{
+ if(img->data) free(img->data);
+ if(img->raw) free(img->raw);
+ memset(img, 0, sizeof(*img));
+}
+
+static bool verify_dart_pair(const char *fast_name, const char *best_name)
+{
+ char path[PATH_MAX];
+ char fast_path[PATH_MAX];
+ char best_path[PATH_MAX];
+
+ getcwd(path, PATH_MAX);
+ snprintf(fast_path, PATH_MAX, "%s/data/%s", path, fast_name);
+ snprintf(best_path, PATH_MAX, "%s/data/%s", path, best_name);
+
+ dart_image_t fast_img, best_img;
+
+ if(!load_dart_image(fast_path, &fast_img))
+ {
+ printf(" SKIP: cannot load %s\n", fast_name);
+ return false;
+ }
+ if(!load_dart_image(best_path, &best_img))
+ {
+ printf(" SKIP: cannot load %s\n", best_name);
+ free_dart_image(&fast_img);
+ return false;
+ }
+
+ if(fast_img.header.srcCmp != DART_COMPRESS_RLE || best_img.header.srcCmp != DART_COMPRESS_LZH)
+ {
+ printf(" SKIP: unexpected compression types\n");
+ free_dart_image(&fast_img);
+ free_dart_image(&best_img);
+ return false;
+ }
+
+ size_t in_pos = best_img.data_offset;
+ size_t ref_block_idx = 0;
+ int blocks_ok = 0;
+ int blocks_fail = 0;
+
+ for(int i = 0; i < best_img.num_blocks; i++)
+ {
+ if(best_img.block_lengths[i] == 0)
+ {
+ if(fast_img.block_lengths[i] != 0) ref_block_idx++;
+ continue;
+ }
+
+ const uint8_t *reference = fast_img.data + ref_block_idx * DART_BUFFER_SIZE;
+
+ if(best_img.block_lengths[i] == -1)
+ {
+ if(memcmp(best_img.raw + in_pos, reference, DART_BUFFER_SIZE) == 0)
+ blocks_ok++;
+ else
+ blocks_fail++;
+ in_pos += DART_BUFFER_SIZE;
+ }
+ else
+ {
+ size_t comp_size = (size_t)best_img.block_lengths[i];
+ uint8_t out[DART_BUFFER_SIZE];
+ size_t out_len = DART_BUFFER_SIZE;
+
+ memset(out, 0, DART_BUFFER_SIZE);
+
+ int ret = AARU_apple_lzh_decode_buffer(out, &out_len, best_img.raw + in_pos, comp_size);
+ if(ret == 0 && out_len == DART_BUFFER_SIZE && memcmp(out, reference, DART_BUFFER_SIZE) == 0)
+ blocks_ok++;
+ else
+ blocks_fail++;
+ in_pos += comp_size;
+ }
+ ref_block_idx++;
+ }
+
+ printf(" %s: %d OK, %d FAIL\n", best_name, blocks_ok, blocks_fail);
+
+ free_dart_image(&fast_img);
+ free_dart_image(&best_img);
+
+ return blocks_fail == 0 && blocks_ok > 0;
+}
+
+TEST(DartAppleLzh, mf2dd_hfs) { EXPECT_TRUE(verify_dart_pair("mf2dd_hfs_fast.dart.lz", "mf2dd_hfs_best.dart.lz")); }
+
+TEST(DartAppleLzh, mf2dd_mfs) { EXPECT_TRUE(verify_dart_pair("mf2dd_mfs_fast.dart.lz", "mf2dd_mfs_best.dart.lz")); }
+
+TEST(DartAppleLzh, mf1dd_hfs) { EXPECT_TRUE(verify_dart_pair("mf1dd_hfs_fast.dart.lz", "mf1dd_hfs_best.dart.lz")); }
+
+TEST(DartAppleLzh, mf1dd_mfs) { EXPECT_TRUE(verify_dart_pair("mf1dd_mfs_fast.dart.lz", "mf1dd_mfs_best.dart.lz")); }
diff --git a/tests/dart/dart_helpers.h b/tests/dart/dart_helpers.h
new file mode 100644
index 0000000..613fc98
--- /dev/null
+++ b/tests/dart/dart_helpers.h
@@ -0,0 +1,99 @@
+/*
+ * 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_TESTS_DART_HELPERS_H
+#define AARU_TESTS_DART_HELPERS_H
+
+#include
+#include
+
+// DART header constants
+#define DART_COMPRESS_RLE 0
+#define DART_COMPRESS_LZH 1
+#define DART_COMPRESS_NONE 2
+
+#define DART_SECTORS_PER_BLOCK 40
+#define DART_SECTOR_SIZE 512
+#define DART_TAG_SECTOR_SIZE 12
+#define DART_DATA_SIZE (DART_SECTORS_PER_BLOCK * DART_SECTOR_SIZE) // 20480
+#define DART_TAG_SIZE (DART_SECTORS_PER_BLOCK * DART_TAG_SECTOR_SIZE) // 480
+#define DART_BUFFER_SIZE (DART_DATA_SIZE + DART_TAG_SIZE) // 20960
+
+#define DART_BLOCK_ARRAY_LEN_LOW 40
+#define DART_BLOCK_ARRAY_LEN_HIGH 72
+
+#define DART_HEADER_SIZE 4
+
+// DART disk types
+#define DART_TYPE_MAC 1
+#define DART_TYPE_LISA 2
+#define DART_TYPE_APPLE2 3
+#define DART_TYPE_MAC_HD 16
+#define DART_TYPE_DOS 17
+#define DART_TYPE_DOS_HD 18
+
+#pragma pack(push, 1)
+
+typedef struct
+{
+ uint8_t srcCmp; // Compression type (0=RLE, 1=LZH, 2=None)
+ uint8_t srcType; // Disk type
+ uint16_t srcSize; // Size in blocks (big-endian)
+} dart_header_t;
+
+#pragma pack(pop)
+
+static inline uint16_t be16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); }
+
+static inline int16_t be16s(const uint8_t *p) { return (int16_t)((p[0] << 8) | p[1]); }
+
+// Returns the number of block length entries based on disk type
+static inline int dart_block_array_len(uint8_t srcType)
+{
+ switch(srcType)
+ {
+ case DART_TYPE_MAC_HD:
+ case DART_TYPE_DOS_HD:
+ return DART_BLOCK_ARRAY_LEN_HIGH;
+ default:
+ return DART_BLOCK_ARRAY_LEN_LOW;
+ }
+}
+
+// Parse DART header from raw data. Returns the offset past the block lengths array.
+static inline size_t dart_parse_header(const uint8_t *data, size_t data_len, dart_header_t *hdr, int16_t *block_lengths,
+ int *num_blocks)
+{
+ if(data_len < DART_HEADER_SIZE) return 0;
+
+ hdr->srcCmp = data[0];
+ hdr->srcType = data[1];
+ hdr->srcSize = be16(data + 2);
+
+ *num_blocks = dart_block_array_len(hdr->srcType);
+ size_t needed = DART_HEADER_SIZE + (*num_blocks) * 2;
+
+ if(data_len < needed) return 0;
+
+ const uint8_t *blk_ptr = data + DART_HEADER_SIZE;
+ for(int i = 0; i < *num_blocks; i++) { block_lengths[i] = be16s(blk_ptr + i * 2); }
+
+ return needed;
+}
+
+#endif // AARU_TESTS_DART_HELPERS_H
diff --git a/tests/data/mf1dd_hfs_best.dart.lz b/tests/data/mf1dd_hfs_best.dart.lz
new file mode 100644
index 0000000..9a36d64
Binary files /dev/null and b/tests/data/mf1dd_hfs_best.dart.lz differ
diff --git a/tests/data/mf1dd_hfs_fast.dart.lz b/tests/data/mf1dd_hfs_fast.dart.lz
new file mode 100644
index 0000000..9b897c6
Binary files /dev/null and b/tests/data/mf1dd_hfs_fast.dart.lz differ
diff --git a/tests/data/mf1dd_mfs_best.dart.lz b/tests/data/mf1dd_mfs_best.dart.lz
new file mode 100644
index 0000000..b3f995f
Binary files /dev/null and b/tests/data/mf1dd_mfs_best.dart.lz differ
diff --git a/tests/data/mf1dd_mfs_fast.dart.lz b/tests/data/mf1dd_mfs_fast.dart.lz
new file mode 100644
index 0000000..517e1f3
Binary files /dev/null and b/tests/data/mf1dd_mfs_fast.dart.lz differ
diff --git a/tests/data/mf2dd_hfs_best.dart.lz b/tests/data/mf2dd_hfs_best.dart.lz
new file mode 100644
index 0000000..83be36c
Binary files /dev/null and b/tests/data/mf2dd_hfs_best.dart.lz differ
diff --git a/tests/data/mf2dd_hfs_fast.dart.lz b/tests/data/mf2dd_hfs_fast.dart.lz
new file mode 100644
index 0000000..8f72480
Binary files /dev/null and b/tests/data/mf2dd_hfs_fast.dart.lz differ
diff --git a/tests/data/mf2dd_mfs_best.dart.lz b/tests/data/mf2dd_mfs_best.dart.lz
new file mode 100644
index 0000000..8993c21
Binary files /dev/null and b/tests/data/mf2dd_mfs_best.dart.lz differ
diff --git a/tests/data/mf2dd_mfs_fast.dart.lz b/tests/data/mf2dd_mfs_fast.dart.lz
new file mode 100644
index 0000000..fed435d
Binary files /dev/null and b/tests/data/mf2dd_mfs_fast.dart.lz differ
diff --git a/tests/tests_run b/tests/tests_run
new file mode 100755
index 0000000..4d4b4af
Binary files /dev/null and b/tests/tests_run differ