diff --git a/src/ngcw/ngcw_junk.c b/src/ngcw/ngcw_junk.c
new file mode 100644
index 0000000..c8d3051
--- /dev/null
+++ b/src/ngcw/ngcw_junk.c
@@ -0,0 +1,229 @@
+/*
+ * 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; version 2.1 of the License.
+ *
+ * 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
+ * .
+ *
+ * Nintendo GameCube/Wii junk map: serialization, deserialization, regeneration.
+ */
+
+#include
+#include
+
+#include
+
+#include "ngcw_junk.h"
+
+/* ---- Little-endian helpers ---- */
+
+static uint16_t read_le16(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); }
+
+static uint32_t read_le32(const uint8_t *p)
+{ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); }
+
+static uint64_t read_le64(const uint8_t *p) { return (uint64_t)read_le32(p) | ((uint64_t)read_le32(p + 4) << 32); }
+
+static void write_le16(uint8_t *p, uint16_t v)
+{
+ p[0] = (uint8_t)(v & 0xFF);
+ p[1] = (uint8_t)((v >> 8) & 0xFF);
+}
+
+static void write_le32(uint8_t *p, uint32_t v)
+{
+ p[0] = (uint8_t)(v & 0xFF);
+ p[1] = (uint8_t)((v >> 8) & 0xFF);
+ p[2] = (uint8_t)((v >> 16) & 0xFF);
+ p[3] = (uint8_t)((v >> 24) & 0xFF);
+}
+
+static void write_le64(uint8_t *p, uint64_t v)
+{
+ write_le32(p, (uint32_t)(v & 0xFFFFFFFF));
+ write_le32(p + 4, (uint32_t)(v >> 32));
+}
+
+/* ---- Serialization ---- */
+
+/*
+ * Format:
+ * [2] version (uint16 LE)
+ * [4] entry_count (uint32 LE)
+ * [2] seed_size (uint16 LE) — NGC_LFG_SEED_SIZE (17)
+ * For each entry:
+ * [8] offset (uint64 LE)
+ * [8] length (uint64 LE)
+ * [2] partition_index (uint16 LE)
+ * [seed_size * 4] seed (raw bytes)
+ *
+ * Header = 8 bytes. Entry = 18 + seed_size * 4 = 86 bytes (when seed_size=17).
+ */
+
+#define JUNK_MAP_HEADER_SIZE 8
+
+int32_t ngcw_serialize_junk_map(const NgcwJunkEntry *entries, uint32_t count, uint8_t **out_data, uint32_t *out_len)
+{
+ if(out_data == NULL || out_len == NULL) return -1;
+
+ uint32_t entry_size = 18 + NGC_LFG_SEED_SIZE * 4;
+ uint32_t size = JUNK_MAP_HEADER_SIZE + count * entry_size;
+ uint8_t *buf = (uint8_t *)malloc(size);
+
+ if(buf == NULL) return -4;
+
+ write_le16(buf, NGCW_JUNK_MAP_VERSION);
+ write_le32(buf + 2, count);
+ write_le16(buf + 6, NGC_LFG_SEED_SIZE);
+
+ for(uint32_t i = 0; i < count; i++)
+ {
+ uint8_t *p = buf + JUNK_MAP_HEADER_SIZE + i * entry_size;
+
+ write_le64(p, entries[i].offset);
+ write_le64(p + 8, entries[i].length);
+ write_le16(p + 16, entries[i].partition_index);
+ memcpy(p + 18, entries[i].seed, NGC_LFG_SEED_SIZE * sizeof(uint32_t));
+ }
+
+ *out_data = buf;
+ *out_len = size;
+ return 0;
+}
+
+int32_t ngcw_deserialize_junk_map(const uint8_t *data, uint32_t data_len, NgcwJunkEntry **entries, uint32_t *count,
+ uint16_t *seed_size)
+{
+ if(data == NULL || entries == NULL || count == NULL || seed_size == NULL) return -1;
+
+ if(data_len < JUNK_MAP_HEADER_SIZE) return -2;
+
+ uint16_t version = read_le16(data);
+ uint32_t entry_cnt = read_le32(data + 2);
+ uint16_t ss = read_le16(data + 6);
+
+ if(version != NGCW_JUNK_MAP_VERSION) return -3;
+
+ if(ss != NGC_LFG_SEED_SIZE) return -3;
+
+ if(entry_cnt == 0)
+ {
+ *entries = NULL;
+ *count = 0;
+ *seed_size = ss;
+ return 0;
+ }
+
+ uint32_t entry_size = 18 + (uint32_t)ss * 4;
+ uint32_t required = JUNK_MAP_HEADER_SIZE + entry_cnt * entry_size;
+
+ if(data_len < required) return -2;
+
+ NgcwJunkEntry *e = (NgcwJunkEntry *)calloc(entry_cnt, sizeof(NgcwJunkEntry));
+
+ if(e == NULL) return -4;
+
+ for(uint32_t i = 0; i < entry_cnt; i++)
+ {
+ const uint8_t *p = data + JUNK_MAP_HEADER_SIZE + i * entry_size;
+
+ e[i].offset = read_le64(p);
+ e[i].length = read_le64(p + 8);
+ e[i].partition_index = read_le16(p + 16);
+ memcpy(e[i].seed, p + 18, ss * sizeof(uint32_t));
+ }
+
+ *entries = e;
+ *count = entry_cnt;
+ *seed_size = ss;
+ return 0;
+}
+
+/* ---- Junk regeneration ---- */
+
+int ngcw_regenerate_junk_sector(const NgcwJunkEntry *entries, uint32_t entry_count, uint64_t disc_offset,
+ uint8_t *output, uint32_t length)
+{
+ if(entries == NULL || entry_count == 0 || output == NULL) return -1;
+
+ /* Binary search for the entry containing disc_offset */
+ int lo = 0;
+ int hi = (int)entry_count - 1;
+
+ while(lo <= hi)
+ {
+ int mid = lo + (hi - lo) / 2;
+ uint64_t entry_end = entries[mid].offset + entries[mid].length;
+
+ if(disc_offset >= entry_end)
+ lo = mid + 1;
+ else if(disc_offset < entries[mid].offset)
+ hi = mid - 1;
+ else
+ {
+ /* Found: disc_offset is within entries[mid] */
+ uint64_t stream_pos = disc_offset - entries[mid].offset;
+
+ struct ngc_lfg_ctx lfg;
+ uint32_t seed_copy[NGC_LFG_SEED_SIZE];
+ memcpy(seed_copy, entries[mid].seed, sizeof(seed_copy));
+ ngc_lfg_set_seed(&lfg, seed_copy);
+
+ /* Advance LFG to the correct stream position */
+ if(stream_pos > 0)
+ {
+ uint8_t discard[4096];
+ size_t rem = (size_t)stream_pos;
+
+ while(rem > 0)
+ {
+ size_t step = rem > sizeof(discard) ? sizeof(discard) : rem;
+ ngc_lfg_get_bytes(&lfg, discard, step);
+ rem -= step;
+ }
+ }
+
+ /* Generate the requested bytes */
+ ngc_lfg_get_bytes(&lfg, output, length);
+ return 0;
+ }
+ }
+
+ return -1; /* Not found */
+}
+
+/* ---- Lazy initialization ---- */
+
+void ngcw_junk_lazy_init(aaruformat_context *ctx)
+{
+ if(ctx == NULL) return;
+
+ if(ctx->ngcw_junk_entries != NULL) return;
+
+ mediaTagEntry *item = NULL;
+ int32_t tag = kMediaTagNgcwJunkMap;
+ HASH_FIND_INT(ctx->mediaTags, &tag, item);
+
+ if(item == NULL || item->length < JUNK_MAP_HEADER_SIZE) return;
+
+ NgcwJunkEntry *entries = NULL;
+ uint32_t count = 0;
+ uint16_t seed_size = 0;
+
+ if(ngcw_deserialize_junk_map(item->data, item->length, &entries, &count, &seed_size) == 0)
+ {
+ ctx->ngcw_junk_entries = entries;
+ ctx->ngcw_junk_entry_count = count;
+ ctx->ngcw_junk_seed_size = seed_size;
+ }
+}
diff --git a/src/ngcw/ngcw_junk.h b/src/ngcw/ngcw_junk.h
new file mode 100644
index 0000000..0222e2f
--- /dev/null
+++ b/src/ngcw/ngcw_junk.h
@@ -0,0 +1,117 @@
+/*
+ * 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; version 2.1 of the License.
+ *
+ * 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
+ * .
+ *
+ * Nintendo GameCube/Wii junk map: serialization, deserialization, regeneration.
+ */
+
+#ifndef LIBAARUFORMAT_NGCW_JUNK_H
+#define LIBAARUFORMAT_NGCW_JUNK_H
+
+#include
+#include
+
+#include "lfg.h"
+
+/* Forward declaration */
+typedef struct aaruformat_context aaruformat_context;
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define NGCW_JUNK_MAP_VERSION 1 /**< Current junk map serialization version. */
+
+ /**
+ * @brief In-memory junk map entry.
+ *
+ * Each entry describes a contiguous region of LFG-generated junk on disc.
+ */
+ typedef struct NgcwJunkEntry
+ {
+ uint64_t offset; /**< Disc byte offset where junk starts. */
+ uint64_t length; /**< Length of junk region in bytes. */
+ uint16_t partition_index; /**< Partition index (0xFFFF for GC / inter-partition). */
+ uint32_t seed[NGC_LFG_SEED_SIZE]; /**< LFG seed (17 words, big-endian). */
+ } NgcwJunkEntry;
+
+ /**
+ * @brief Serialize a junk map for storage as a media tag.
+ *
+ * Format (all little-endian):
+ * [2 bytes] version (uint16)
+ * [4 bytes] entry_count (uint32)
+ * [2 bytes] seed_size (uint16) — NGC_LFG_SEED_SIZE
+ * For each entry (86 bytes when seed_size=17):
+ * [8 bytes] offset (uint64)
+ * [8 bytes] length (uint64)
+ * [2 bytes] partition_index (uint16)
+ * [seed_size * 4 bytes] seed (raw bytes, big-endian uint32 words)
+ *
+ * @param entries Array of junk entries.
+ * @param count Number of entries.
+ * @param out_data Output: malloc'd buffer (caller must free).
+ * @param out_len Output: length of the buffer.
+ * @return 0 on success, negative error code on failure.
+ */
+ int32_t ngcw_serialize_junk_map(const NgcwJunkEntry *entries, uint32_t count, uint8_t **out_data,
+ uint32_t *out_len);
+
+ /**
+ * @brief Deserialize a junk map from a media tag buffer.
+ *
+ * @param data Serialized data buffer.
+ * @param data_len Length of the data buffer.
+ * @param entries Output: malloc'd array of NgcwJunkEntry (caller must free).
+ * @param count Output: number of entries.
+ * @param seed_size Output: seed size in uint32 words.
+ * @return 0 on success, negative error code on failure.
+ */
+ int32_t ngcw_deserialize_junk_map(const uint8_t *data, uint32_t data_len, NgcwJunkEntry **entries, uint32_t *count,
+ uint16_t *seed_size);
+
+ /**
+ * @brief Regenerate a junk sector from the junk map.
+ *
+ * Looks up the given disc byte offset in the junk map, initializes the LFG
+ * with the matching entry's seed, advances to the correct stream position,
+ * and generates the requested number of bytes.
+ *
+ * @param entries Array of junk entries (sorted by offset).
+ * @param entry_count Number of entries.
+ * @param disc_offset Disc byte offset of the sector to regenerate.
+ * @param output Output buffer to fill.
+ * @param length Number of bytes to generate.
+ * @return 0 on success, -1 if the offset is not in the junk map.
+ */
+ int ngcw_regenerate_junk_sector(const NgcwJunkEntry *entries, uint32_t entry_count, uint64_t disc_offset,
+ uint8_t *output, uint32_t length);
+
+ /**
+ * @brief Lazy initialization: load junk map from media tags.
+ *
+ * Populates ctx->ngcw_junk_entries from the kMediaTagNgcwJunkMap media tag.
+ *
+ * @param ctx AaruFormat context.
+ */
+ void ngcw_junk_lazy_init(aaruformat_context *ctx);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LIBAARUFORMAT_NGCW_JUNK_H */