diff --git a/CMakeLists.txt b/CMakeLists.txt
index 513c36a..46de9f6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -261,6 +261,7 @@ add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h a
stuffit/method13.c
stuffit/method13_tables.h
stuffit/method14.c
+ stuffit/shrinkwrap.c
stuffit/arsenic.c
stuffit/brimstone.c
stuffit/cyanide.c
diff --git a/library.c b/library.c
index aaec963..2a21b05 100644
--- a/library.c
+++ b/library.c
@@ -629,10 +629,17 @@ AARU_EXPORT int AARU_CALL AARU_stuffit_arsenic_decode_buffer(uint8_t *dst_buffer
const uint8_t *src_buffer, size_t src_size)
{ return stuffit_arsenic_decode_buffer(dst_buffer, dst_size, src_buffer, src_size); }
+AARU_EXPORT int AARU_CALL AARU_stuffit_shrinkwrap_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
+ const uint8_t *src_buffer, size_t src_size)
+{ return stuffit_shrinkwrap_decode_buffer(dst_buffer, dst_size, src_buffer, src_size); }
+
AARU_EXPORT int AARU_CALL AARU_stuffitx_brimstone_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size,
int max_order, int sub_alloc_size)
-{ return stuffitx_brimstone_decode_buffer(dst_buffer, dst_size, src_buffer, src_size, max_order, sub_alloc_size, NULL); }
+{
+ return stuffitx_brimstone_decode_buffer(dst_buffer, dst_size, src_buffer, src_size, max_order, sub_alloc_size,
+ NULL);
+}
AARU_EXPORT int AARU_CALL AARU_stuffitx_cyanide_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size)
diff --git a/library.h b/library.h
index c9bb2e3..0cc4a1c 100644
--- a/library.h
+++ b/library.h
@@ -400,6 +400,10 @@ AARU_EXPORT int AARU_CALL AARU_stuffit_method14_decode_buffer(uint8_t *dst_buffe
AARU_EXPORT int AARU_CALL AARU_stuffit_arsenic_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size);
+// ShrinkWrap 3 SIT2 compression (NDIF chunk type 0xF0)
+AARU_EXPORT int AARU_CALL AARU_stuffit_shrinkwrap_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
+ const uint8_t *src_buffer, size_t src_size);
+
// StuffIt X method 0: Brimstone (PPMd Variant G)
AARU_EXPORT int AARU_CALL AARU_stuffitx_brimstone_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size,
diff --git a/stuffit/shrinkwrap.c b/stuffit/shrinkwrap.c
new file mode 100644
index 0000000..59431b5
--- /dev/null
+++ b/stuffit/shrinkwrap.c
@@ -0,0 +1,322 @@
+/*
+ * 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 .
+ */
+
+/*
+ * ShrinkWrap 3 "SIT2" decompressor
+ *
+ * Reverse-engineered from 68K 'Dcmp' resource ID 240 in ShrinkWrap 3.5.1.
+ * Used by NDIF disk images (chunk type 0xF0) created by ShrinkWrap 3.
+ * Component type 'Chdc', subtype 'SIT2', manufacturer 'Wrap'.
+ *
+ * Algorithm: Block Huffman LZSS, adapted from StuffIt method 14, with:
+ * - 276 literal/length symbols, 16 distance symbols
+ * - 20 length codes, 16 distance codes
+ * - 16-byte per-chunk header
+ */
+
+#include
+#include
+#include "stuffit.h"
+#include "stuffit_internal.h"
+
+#define SW_LITLEN 276
+#define SW_DIST 16
+
+/* Quicksort for canonical code assignment (same as method 14) */
+static void sw_update(uint16_t first, uint16_t last, uint8_t *code, uint16_t *freq)
+{
+ while(last - first > 1)
+ {
+ uint16_t i = first, j = last;
+ do
+ {
+ while(++i < last && code[first] > code[i]);
+ while(--j > first && code[first] < code[j]);
+ if(j > i)
+ {
+ uint8_t tc = code[i];
+ code[i] = code[j];
+ code[j] = tc;
+ uint16_t tf = freq[i];
+ freq[i] = freq[j];
+ freq[j] = tf;
+ }
+ } while(j > i);
+
+ if(first != j)
+ {
+ uint8_t tc = code[first];
+ code[first] = code[j];
+ code[j] = tc;
+ uint16_t tf = freq[first];
+ freq[first] = freq[j];
+ freq[j] = tf;
+ i = j + 1;
+ if(last - i <= j - first)
+ {
+ sw_update(i, last, code, freq);
+ last = j;
+ }
+ else
+ {
+ sw_update(first, j, code, freq);
+ first = i;
+ }
+ }
+ else
+ ++first;
+ }
+}
+
+/* Read Huffman tree from bitstream (same structure as method 14) */
+static void sw_read_tree(StuffitBitReaderLE *br, uint16_t codesize, uint8_t *code, uint8_t *codecopy, uint16_t *freq,
+ uint32_t *buff, uint16_t *result)
+{
+ uint32_t k = stuffit_bits_le_read(br, 1);
+ uint32_t j = stuffit_bits_le_read(br, 2) + 2;
+ uint32_t o = stuffit_bits_le_read(br, 3) + 1;
+ uint32_t size = 1u << j;
+ uint32_t m = size - 1;
+ k = k ? m - 1 : (uint32_t)-1;
+
+ if(stuffit_bits_le_read(br, 2) & 1)
+ {
+ uint16_t temp_result[64];
+ uint8_t temp_code[32], temp_copy[32];
+ uint16_t temp_freq[64];
+ uint32_t temp_buff[32];
+ memset(temp_result, 0, sizeof(temp_result));
+ sw_read_tree(br, size, temp_code, temp_copy, temp_freq, temp_buff, temp_result);
+
+ for(uint32_t i = 0; i < codesize;)
+ {
+ uint32_t l = 0, n;
+ do
+ {
+ l = temp_result[l + stuffit_bits_le_read(br, 1)];
+ n = size << 1;
+ } while(n > l);
+ l -= n;
+ if(k != l)
+ {
+ if(l == m)
+ {
+ l = 0;
+ do
+ {
+ l = temp_result[l + stuffit_bits_le_read(br, 1)];
+ n = size << 1;
+ } while(n > l);
+ l += 3 - n;
+ while(l-- && i < codesize)
+ {
+ code[i] = code[i - 1];
+ i++;
+ }
+ }
+ else
+ code[i++] = l + o;
+ }
+ else
+ code[i++] = 0;
+ }
+ }
+ else
+ {
+ for(uint32_t i = 0; i < codesize;)
+ {
+ uint32_t l = stuffit_bits_le_read(br, j);
+ if(k != l)
+ {
+ if(l == m)
+ {
+ l = stuffit_bits_le_read(br, j) + 3;
+ while(l-- && i < codesize)
+ {
+ code[i] = code[i - 1];
+ i++;
+ }
+ }
+ else
+ code[i++] = l + o;
+ }
+ else
+ code[i++] = 0;
+ }
+ }
+
+ /* Sort + canonical code assignment + tree build */
+ for(uint32_t i = 0; i < codesize; i++)
+ {
+ codecopy[i] = code[i];
+ freq[i] = i;
+ }
+ sw_update(0, codesize, codecopy, freq);
+
+ uint32_t i;
+ for(i = 0; i < codesize && !codecopy[i]; i++);
+ for(j = 0; i < codesize; i++, j++)
+ {
+ if(i) j <<= (codecopy[i] - codecopy[i - 1]);
+ k = codecopy[i];
+ m = 0;
+ for(uint32_t l = j; k--; l >>= 1) m = (m << 1) | (l & 1);
+ buff[freq[i]] = m;
+ }
+
+ for(i = 0; i < codesize * 2; i++) result[i] = 0;
+ j = 2;
+ for(i = 0; i < codesize; i++)
+ {
+ uint32_t l = 0;
+ m = buff[i];
+ for(k = 0; k < code[i]; k++)
+ {
+ l += (m & 1);
+ if(code[i] - 1 <= k)
+ result[l] = codesize * 2 + i;
+ else
+ {
+ if(!result[l])
+ {
+ result[l] = j;
+ j += 2;
+ }
+ l = result[l];
+ }
+ m >>= 1;
+ }
+ }
+
+ /* Byte-align after tree read (68K code length reader returns byte-aligned) */
+ br->bits_left = 0;
+ br->buffer = 0;
+}
+
+int stuffit_shrinkwrap_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t *src, size_t src_size)
+{
+ if(src_size < 16) return -1;
+
+ /* 16-byte header: skip4 | LE32 decomp_size | LE32 decomp_size | FFFFFFFF */
+ uint32_t decomp_size =
+ (uint32_t)src[4] | ((uint32_t)src[5] << 8) | ((uint32_t)src[6] << 16) | ((uint32_t)src[7] << 24);
+ if(decomp_size > *dst_size) return -1;
+
+ /* Build length/distance tables */
+ uint8_t len_extra[20];
+ uint16_t len_base[20];
+ uint8_t dist_extra[16];
+ uint16_t dist_base[16];
+
+ uint32_t i, k;
+ for(i = 0, k = 0; i < 20; i++)
+ {
+ len_base[i] = k + 4;
+ len_extra[i] = (i >= 2) ? ((i - 2) >> 1) : 0;
+ k += (1u << len_extra[i]);
+ }
+ for(i = 0, k = 1; i < 16; i++)
+ {
+ dist_base[i] = k;
+ dist_extra[i] = i;
+ k += (1u << i);
+ }
+
+ /* Allocate workspace */
+ uint8_t *code = calloc(SW_LITLEN + 64, 1);
+ uint8_t *codecopy = calloc(SW_LITLEN + 64, 1);
+ uint16_t *freq = calloc(SW_LITLEN + 64, sizeof(uint16_t));
+ uint32_t *buff = calloc(SW_LITLEN + 64, sizeof(uint32_t));
+ uint16_t *litlen_tree = calloc(SW_LITLEN * 8, sizeof(uint16_t));
+ uint16_t *dist_tree = calloc(4096, sizeof(uint16_t));
+
+ if(!code || !codecopy || !freq || !buff || !litlen_tree || !dist_tree) goto fail;
+
+ StuffitBitReaderLE br;
+ stuffit_bits_le_init(&br, src + 16, src_size - 16);
+
+ /* Read litlen tree (276 symbols) */
+ memset(litlen_tree, 0, SW_LITLEN * 8 * sizeof(uint16_t));
+ sw_read_tree(&br, SW_LITLEN, code, codecopy, freq, buff, litlen_tree);
+
+ /* Read dist tree (16 symbols) */
+ memset(code, 0, SW_LITLEN + 64);
+ memset(dist_tree, 0, 4096 * sizeof(uint16_t));
+ sw_read_tree(&br, SW_DIST, code, codecopy, freq, buff, dist_tree);
+
+ /* Main decode loop */
+ size_t di = 0;
+ uint32_t litlen_th = SW_LITLEN * 2;
+ uint32_t dist_th = SW_DIST * 2;
+
+ while(di < decomp_size)
+ {
+ /* Decode literal/length symbol */
+ i = 0;
+ while(i < litlen_th) i = litlen_tree[i + stuffit_bits_le_read(&br, 1)];
+ i -= litlen_th;
+
+ if(i < 256) { dst[di++] = (uint8_t)i; }
+ else
+ {
+ i -= 256;
+ if(i >= 20) goto fail;
+
+ k = len_base[i];
+ uint32_t extra = len_extra[i];
+ if(extra) k += stuffit_bits_le_read(&br, extra);
+
+ /* Decode distance */
+ uint32_t d = 0;
+ while(d < dist_th) d = dist_tree[d + stuffit_bits_le_read(&br, 1)];
+ d -= dist_th;
+ if(d >= 16) goto fail;
+
+ uint32_t dist = dist_base[d];
+ extra = dist_extra[d];
+ if(extra) dist += stuffit_bits_le_read(&br, extra);
+
+ if(dist > di) goto fail;
+
+ /* Copy from back-reference */
+ for(uint32_t j2 = 0; j2 < k && di < decomp_size; j2++)
+ {
+ dst[di] = dst[di - dist];
+ di++;
+ }
+ }
+ }
+
+ free(code);
+ free(codecopy);
+ free(freq);
+ free(buff);
+ free(litlen_tree);
+ free(dist_tree);
+ *dst_size = di;
+ return 0;
+
+fail:
+ free(code);
+ free(codecopy);
+ free(freq);
+ free(buff);
+ free(litlen_tree);
+ free(dist_tree);
+ return -1;
+}
diff --git a/stuffit/stuffit.h b/stuffit/stuffit.h
index 3b97446..71a65d2 100644
--- a/stuffit/stuffit.h
+++ b/stuffit/stuffit.h
@@ -32,6 +32,9 @@ int stuffit_method13_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t
int stuffit_method14_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t *src, size_t src_size);
int stuffit_arsenic_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t *src, size_t src_size);
+/* ShrinkWrap 3 SIT2 compression (used by NDIF chunk type 0xF0) */
+int stuffit_shrinkwrap_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t *src, size_t src_size);
+
/* StuffIt X compression methods */
int stuffitx_brimstone_decode_buffer(uint8_t *dst, size_t *dst_size, const uint8_t *src, size_t src_size, int max_order,
int sub_alloc_size, size_t *src_consumed);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fa6e371..5fb17bf 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -180,6 +180,9 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffit_method13_v5.bin
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffit_arsenic.bin
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffit_shrinkwrap.bin
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
+
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/stuffitx_brimstone.bin
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
diff --git a/tests/data/stuffit_shrinkwrap.bin b/tests/data/stuffit_shrinkwrap.bin
new file mode 100644
index 0000000..8435d92
Binary files /dev/null and b/tests/data/stuffit_shrinkwrap.bin differ
diff --git a/tests/stuffit/stuffit.cpp b/tests/stuffit/stuffit.cpp
index 0851203..fa96036 100644
--- a/tests/stuffit/stuffit.cpp
+++ b/tests/stuffit/stuffit.cpp
@@ -391,3 +391,36 @@ TEST_F(StuffitxBlendFixture, stuffitx_blend)
free(outBuf);
EXPECT_EQ(crc, EXPECTED_CRC32);
}
+
+/* ---- ShrinkWrap 3 SIT2 (NDIF chunk type 0xF0) ---- */
+
+#define SHRINKWRAP_SIZE 27219
+#define SHRINKWRAP_ORIGSIZE 65536
+#define SHRINKWRAP_CRC32 0x93A5E340
+
+class StuffitShrinkwrapFixture : public ::testing::Test
+{
+protected:
+ uint8_t *buf = nullptr;
+
+ void SetUp() { buf = load_test_file("stuffit_shrinkwrap.bin", SHRINKWRAP_SIZE); }
+
+ void TearDown() { free(buf); }
+};
+
+TEST_F(StuffitShrinkwrapFixture, stuffit_shrinkwrap)
+{
+ ASSERT_NE(buf, nullptr);
+
+ size_t destLen = SHRINKWRAP_ORIGSIZE;
+ auto *outBuf = (uint8_t *)malloc(SHRINKWRAP_ORIGSIZE);
+
+ auto err = AARU_stuffit_shrinkwrap_decode_buffer(outBuf, &destLen, buf, SHRINKWRAP_SIZE);
+
+ EXPECT_EQ(err, 0);
+ EXPECT_EQ(destLen, (size_t)SHRINKWRAP_ORIGSIZE);
+
+ auto crc = crc32_data(outBuf, destLen);
+ free(outBuf);
+ EXPECT_EQ(crc, (uint32_t)SHRINKWRAP_CRC32);
+}