diff --git a/CMakeLists.txt b/CMakeLists.txt
index b46c5e4..28ea6e1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -308,6 +308,8 @@ add_library(aaruformat
src/lib/gf256.h
src/lib/reed_solomon.c
src/lib/reed_solomon.h
+ src/erasure.c
+ include/erasure_internal.h
include/aaruformat/structs/erasure.h)
# Set up include directories for the target
diff --git a/include/aaruformat/context.h b/include/aaruformat/context.h
index 2eb174f..6cc9b92 100644
--- a/include/aaruformat/context.h
+++ b/include/aaruformat/context.h
@@ -374,6 +374,21 @@ typedef struct aaruformat_context
uint64_t wii_cached_physical_group; ///< Physical group number of cached block
bool wii_cache_valid; ///< Whether the encrypted group cache is valid
bool wii_building_crypto_block; ///< True while gathering sectors for re-encryption (suppresses recursion)
+
+ /* Erasure coding (write path) */
+ uint8_t ec_algorithm; ///< ErasureCodingAlgorithm (0=XOR, 1=RS-Vandermonde).
+ uint16_t ec_K; ///< Data blocks per stripe.
+ uint16_t ec_M; ///< Parity blocks per stripe.
+ uint32_t ec_data_shard_size; ///< Max on-disk block size for data blocks (fixed at creation).
+ void *ec_rs_ctx; ///< rs_context* (opaque RS codec), NULL if EC disabled.
+ uint8_t **ec_data_parity; ///< Array of K * M parity buffers (interleaved stripe slots).
+ uint64_t *ec_data_block_offsets; ///< Array of K * K file offsets for blocks in active stripes.
+ uint32_t *ec_data_block_sizes; ///< Array of K * K actual on-disk sizes for blocks in active stripes.
+ uint64_t *ec_data_shard_crcs; ///< Array of K * K CRC64 values for blocks in active stripes.
+ uint16_t *ec_data_stripe_counts; ///< Array of K: blocks accumulated per stripe slot.
+ uint32_t ec_total_data_blocks; ///< Total data blocks written (counter for round-robin assignment).
+ UT_array *ec_data_stripes; ///< Completed data stripe descriptors (serialized to ECMB).
+ bool ec_enabled; ///< True if erasure coding is active.
} aaruformat_context;
#ifndef AARUFORMAT_CONTEXT_DECLARED
diff --git a/include/aaruformat/decls.h b/include/aaruformat/decls.h
index 338ae46..e3d3d80 100644
--- a/include/aaruformat/decls.h
+++ b/include/aaruformat/decls.h
@@ -292,4 +292,7 @@ AARU_EXPORT int have_arm_crypto();
AARU_EXPORT TARGET_WITH_SIMD uint64_t AARU_CALL aaruf_crc64_vmull(uint64_t previous_crc, const uint8_t *data, long len);
#endif
+/* Erasure coding */
+AARU_EXPORT int32_t AARU_CALL aaruf_set_erasure_coding(void *context, uint8_t algorithm, uint16_t K, uint16_t M);
+
#endif // LIBAARUFORMAT_DECLS_H
diff --git a/include/erasure_internal.h b/include/erasure_internal.h
new file mode 100644
index 0000000..39ce88b
--- /dev/null
+++ b/include/erasure_internal.h
@@ -0,0 +1,52 @@
+/*
+ * 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 LIBAARUFORMAT_ERASURE_INTERNAL_H
+#define LIBAARUFORMAT_ERASURE_INTERNAL_H
+
+#include "aaruformat/context.h"
+#include "aaruformat/structs/data.h"
+
+/**
+ * @brief Accumulate parity for a data block just written to disk.
+ *
+ * Called from aaruf_close_current_block() after writing header + payload.
+ */
+void ec_accumulate_data_block(aaruformat_context *ctx, const BlockHeader *block_header, const uint8_t *lzma_props,
+ const uint8_t *payload, uint32_t payload_size, uint64_t file_offset);
+
+/**
+ * @brief Flush a completed data stripe slot: write parity blocks and record descriptor.
+ */
+void ec_flush_data_stripe(aaruformat_context *ctx, uint32_t slot);
+
+/**
+ * @brief Flush partial stripes, write ECMB and recovery footer.
+ *
+ * Called from aaruf_finalize_write() after index is written.
+ */
+void ec_finalize(aaruformat_context *ctx);
+
+/**
+ * @brief Free all erasure coding state.
+ *
+ * Called from aaruf_close().
+ */
+void ec_free(aaruformat_context *ctx);
+
+#endif /* LIBAARUFORMAT_ERASURE_INTERNAL_H */
diff --git a/src/close.c b/src/close.c
index 3e74d86..1da1ae7 100644
--- a/src/close.c
+++ b/src/close.c
@@ -42,6 +42,7 @@
#include
+#include "erasure_internal.h"
#include "internal.h"
#include "log.h"
@@ -281,6 +282,9 @@ AARU_EXPORT int AARU_CALL aaruf_close(void *context)
free(ctx->user_data_ddt2); // v2 DDT primary/secondary
free(ctx->cached_secondary_ddt2); // Cached secondary DDT (read operations)
+ // Free erasure coding state
+ ec_free(ctx);
+
// Free LRU caches (uses cache->free_func to free cached values)
free_cache(&ctx->block_header_cache);
free_cache(&ctx->block_cache);
diff --git a/src/close_write.c b/src/close_write.c
index c0fa815..239fc22 100644
--- a/src/close_write.c
+++ b/src/close_write.c
@@ -36,6 +36,7 @@
#include
+#include "erasure_internal.h"
#include "internal.h"
#include "log.h"
@@ -5383,6 +5384,9 @@ int32_t aaruf_finalize_write(aaruformat_context *ctx)
if(res != AARUF_STATUS_OK) return res;
}
+ // Write erasure coding parity, ECMB, and recovery footer
+ ec_finalize(ctx);
+
if(ctx->deduplicate && ctx->sector_hash_map != NULL)
{
TRACE("Clearing sector hash map");
diff --git a/src/erasure.c b/src/erasure.c
new file mode 100644
index 0000000..38219ba
--- /dev/null
+++ b/src/erasure.c
@@ -0,0 +1,567 @@
+/*
+ * 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 .
+ */
+
+/**
+ * @file erasure.c
+ * @brief Erasure coding setup, parity accumulation, and write-path integration.
+ */
+
+#include
+#include
+
+#include "aaruformat.h"
+#include "aaruformat/context.h"
+#include "aaruformat/consts.h"
+#include "aaruformat/enums.h"
+#include "aaruformat/errors.h"
+#include "aaruformat/structs/data.h"
+#include "aaruformat/structs/erasure.h"
+#include "internal.h"
+#include "log.h"
+#include "lib/gf256.h"
+#include "lib/reed_solomon.h"
+
+/* UT_array icd for completed stripe descriptors.
+ * Each descriptor is a variable-length blob serialized in-place. We store
+ * them as flat byte buffers since the size per stripe depends on K and M. */
+static UT_icd ec_stripe_icd = {sizeof(uint8_t), NULL, NULL, NULL};
+
+/* Forward declarations */
+void ec_flush_data_stripe(aaruformat_context *ctx, uint32_t slot);
+
+/**
+ * @brief Configure erasure coding for a newly created image.
+ *
+ * Must be called after aaruf_create() and before the first aaruf_write_sector().
+ * Allocates K interleaved stripe slots, each with M parity buffers.
+ *
+ * @param context Opaque context from aaruf_create().
+ * @param algorithm ErasureCodingAlgorithm (0=XOR, 1=RS).
+ * @param K Data blocks per stripe (>= 1).
+ * @param M Parity blocks per stripe (>= 1).
+ * @return AARUF_STATUS_OK on success, error code otherwise.
+ */
+AARU_EXPORT int32_t AARU_CALL aaruf_set_erasure_coding(void *context, uint8_t algorithm, uint16_t K, uint16_t M)
+{
+ TRACE("Entering aaruf_set_erasure_coding(%p, %u, %u, %u)", context, algorithm, K, M);
+
+ if(context == NULL) return AARUF_STATUS_INVALID_CONTEXT;
+
+ aaruformat_context *ctx = (aaruformat_context *)context;
+ if(ctx->magic != AARU_MAGIC) return AARUF_STATUS_INVALID_CONTEXT;
+ if(!ctx->is_writing) return AARUF_READ_ONLY;
+
+ /* Validate parameters */
+ if(K == 0 || M == 0) return AARUF_ERROR_INCORRECT_DATA_SIZE;
+ if((uint32_t)K + M > 255) return AARUF_ERROR_INCORRECT_DATA_SIZE;
+ if(algorithm == kErasureCodingXor && M != 1) return AARUF_ERROR_INCORRECT_DATA_SIZE;
+
+ /* Compute data shard size: max possible on-disk block size.
+ * = sizeof(BlockHeader) + LZMA_PROPERTIES_LENGTH + (1 << dataShift) * sectorSize
+ * This is the worst case: uncompressed block + LZMA properties header. */
+ uint32_t sectors_per_block = 1U << ctx->user_data_ddt_header.dataShift;
+ uint32_t max_payload = sectors_per_block * ctx->current_block_header.sectorSize;
+ if(max_payload == 0) max_payload = sectors_per_block * 512; /* fallback if sectorSize not yet set */
+ uint32_t shard_size = (uint32_t)sizeof(BlockHeader) + LZMA_PROPERTIES_LENGTH + max_payload;
+
+ /* Create RS codec */
+ rs_context *rs = rs_create(K, M);
+ if(!rs) return AARUF_ERROR_NOT_ENOUGH_MEMORY;
+
+ /* Allocate K stripe slots × M parity buffers */
+ uint8_t **parity = (uint8_t **)calloc((size_t)K * M, sizeof(uint8_t *));
+ if(!parity) { rs_free(rs); return AARUF_ERROR_NOT_ENOUGH_MEMORY; }
+
+ for(uint32_t i = 0; i < (uint32_t)K * M; i++)
+ {
+ parity[i] = (uint8_t *)calloc(1, shard_size);
+ if(!parity[i])
+ {
+ for(uint32_t j = 0; j < i; j++) free(parity[j]);
+ free(parity);
+ rs_free(rs);
+ return AARUF_ERROR_NOT_ENOUGH_MEMORY;
+ }
+ }
+
+ /* Allocate tracking arrays (K entries per slot × K slots) */
+ uint64_t *offsets = (uint64_t *)calloc((size_t)K * K, sizeof(uint64_t));
+ uint32_t *sizes = (uint32_t *)calloc((size_t)K * K, sizeof(uint32_t));
+ uint64_t *crcs = (uint64_t *)calloc((size_t)K * K, sizeof(uint64_t));
+ uint16_t *counts = (uint16_t *)calloc(K, sizeof(uint16_t));
+
+ if(!offsets || !sizes || !crcs || !counts)
+ {
+ free(offsets); free(sizes); free(crcs); free(counts);
+ for(uint32_t i = 0; i < (uint32_t)K * M; i++) free(parity[i]);
+ free(parity);
+ rs_free(rs);
+ return AARUF_ERROR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* Initialize completed stripes array */
+ UT_array *stripes = NULL;
+ utarray_new(stripes, &ec_stripe_icd);
+
+ /* Store in context */
+ ctx->ec_algorithm = algorithm;
+ ctx->ec_K = K;
+ ctx->ec_M = M;
+ ctx->ec_data_shard_size = shard_size;
+ ctx->ec_rs_ctx = rs;
+ ctx->ec_data_parity = parity;
+ ctx->ec_data_block_offsets = offsets;
+ ctx->ec_data_block_sizes = sizes;
+ ctx->ec_data_shard_crcs = crcs;
+ ctx->ec_data_stripe_counts = counts;
+ ctx->ec_total_data_blocks = 0;
+ ctx->ec_data_stripes = stripes;
+ ctx->ec_enabled = true;
+
+ /* Set feature flag so old readers know parity data exists */
+ ctx->header.featureCompatibleRo |= AARU_FEATURE_ROCOMPAT_ERASURE;
+
+ TRACE("Erasure coding configured: algorithm=%u K=%u M=%u shard_size=%u", algorithm, K, M, shard_size);
+ TRACE("Exiting aaruf_set_erasure_coding() = 0");
+ return AARUF_STATUS_OK;
+}
+
+/**
+ * @brief Accumulate parity for a data block that was just written to disk.
+ *
+ * Called from aaruf_close_current_block() after writing BlockHeader + payload
+ * to the file but before freeing the compressed data buffer.
+ *
+ * @param ctx Context with EC enabled.
+ * @param block_header Pointer to the BlockHeader that was written.
+ * @param lzma_props LZMA properties (may be NULL if not LZMA).
+ * @param payload Compressed payload data (or uncompressed if no compression).
+ * @param payload_size Size of the payload data.
+ * @param file_offset File offset where the block was written.
+ */
+void ec_accumulate_data_block(aaruformat_context *ctx, const BlockHeader *block_header, const uint8_t *lzma_props,
+ const uint8_t *payload, uint32_t payload_size, uint64_t file_offset)
+{
+ if(!ctx->ec_enabled) return;
+
+ const uint16_t K = ctx->ec_K;
+ const uint16_t M = ctx->ec_M;
+ const uint32_t shard = ctx->ec_data_shard_size;
+
+ /* Determine which stripe slot this block goes to (interleaved round-robin) */
+ uint32_t slot = ctx->ec_total_data_blocks % K;
+
+ /* Position within this slot's stripe */
+ uint16_t pos = ctx->ec_data_stripe_counts[slot];
+
+ /* Build on-disk shard in a temp buffer:
+ * [BlockHeader] [LZMA props if LZMA] [payload]
+ * Remaining bytes to shard_size are implicitly zero (parity buffers were calloc'd) */
+ uint32_t actual_size = (uint32_t)sizeof(BlockHeader);
+ if(block_header->compression == kCompressionLzma && lzma_props)
+ actual_size += LZMA_PROPERTIES_LENGTH;
+ actual_size += payload_size;
+
+ /* We need a temporary flat copy of the on-disk representation for CRC64 and parity accumulation */
+ uint8_t *shard_buf = (uint8_t *)calloc(1, shard);
+ if(!shard_buf) return; /* Best effort — if OOM, skip parity for this block */
+
+ /* Copy BlockHeader */
+ memcpy(shard_buf, block_header, sizeof(BlockHeader));
+ uint32_t offset = sizeof(BlockHeader);
+
+ /* Copy LZMA properties if applicable */
+ if(block_header->compression == kCompressionLzma && lzma_props)
+ {
+ memcpy(shard_buf + offset, lzma_props, LZMA_PROPERTIES_LENGTH);
+ offset += LZMA_PROPERTIES_LENGTH;
+ }
+
+ /* Copy payload */
+ memcpy(shard_buf + offset, payload, payload_size);
+
+ /* Compute CRC64 of the zero-padded shard */
+ uint64_t shard_crc = aaruf_crc64_data(shard_buf, shard);
+
+ /* Record tracking info */
+ size_t tracking_idx = (size_t)slot * K + pos;
+ ctx->ec_data_block_offsets[tracking_idx] = file_offset;
+ ctx->ec_data_block_sizes[tracking_idx] = actual_size;
+ ctx->ec_data_shard_crcs[tracking_idx] = shard_crc;
+
+ /* Accumulate into parity buffers for this slot */
+ for(uint16_t m = 0; m < M; m++)
+ {
+ uint8_t coeff = rs_get_coefficient((rs_context *)ctx->ec_rs_ctx, m, pos);
+ size_t parity_idx = (size_t)slot * M + m;
+ rs_encode_incremental(coeff, shard_buf, ctx->ec_data_parity[parity_idx], shard);
+ }
+
+ free(shard_buf);
+
+ ctx->ec_data_stripe_counts[slot]++;
+ ctx->ec_total_data_blocks++;
+
+ /* Check if this stripe slot is full → write parity blocks */
+ if(ctx->ec_data_stripe_counts[slot] == K)
+ {
+ ec_flush_data_stripe(ctx, slot);
+ }
+}
+
+/**
+ * @brief Write M parity blocks for a completed data stripe slot and record the stripe descriptor.
+ *
+ * @param ctx Context with EC enabled.
+ * @param slot Stripe slot index (0 .. K-1).
+ */
+void ec_flush_data_stripe(aaruformat_context *ctx, uint32_t slot)
+{
+ const uint16_t K = ctx->ec_K;
+ const uint16_t M = ctx->ec_M;
+ const uint32_t shard = ctx->ec_data_shard_size;
+ uint16_t actual_k = ctx->ec_data_stripe_counts[slot];
+
+ if(actual_k == 0) return;
+
+ /* Build and serialize stripe descriptor:
+ * [actualK: uint16_t]
+ * [actualK × StripeDataBlockEntry: offset(8) + onDiskSize(4) + shardCrc64(8) = 20 bytes each]
+ * [M × StripeParityBlockEntry: offset(8) = 8 bytes each]
+ */
+ size_t desc_data_size = sizeof(uint16_t) + (size_t)actual_k * sizeof(StripeDataBlockEntry) +
+ (size_t)M * sizeof(StripeParityBlockEntry);
+ uint8_t *desc = (uint8_t *)calloc(1, desc_data_size);
+ if(!desc) return;
+
+ uint8_t *p = desc;
+
+ /* Write actualK */
+ memcpy(p, &actual_k, sizeof(uint16_t)); p += sizeof(uint16_t);
+
+ /* Write data block entries */
+ for(uint16_t k = 0; k < actual_k; k++)
+ {
+ size_t idx = (size_t)slot * K + k;
+ StripeDataBlockEntry entry;
+ entry.offset = ctx->ec_data_block_offsets[idx];
+ entry.onDiskSize = ctx->ec_data_block_sizes[idx];
+ entry.shardCrc64 = ctx->ec_data_shard_crcs[idx];
+ memcpy(p, &entry, sizeof(StripeDataBlockEntry)); p += sizeof(StripeDataBlockEntry);
+ }
+
+ /* Write M parity blocks to disk */
+ uint64_t alignment_mask = (1ULL << ctx->user_data_ddt_header.blockAlignmentShift) - 1;
+
+ for(uint16_t m = 0; m < M; m++)
+ {
+ size_t parity_idx = (size_t)slot * M + m;
+ uint8_t *parity_data = ctx->ec_data_parity[parity_idx];
+
+ /* Compress the parity shard using the same settings as data blocks */
+ BlockHeader parity_header;
+ memset(&parity_header, 0, sizeof(BlockHeader));
+ parity_header.identifier = DataBlock;
+ parity_header.type = kDataTypeErasureParity;
+ parity_header.compression = kCompressionNone;
+ parity_header.sectorSize = 0;
+ parity_header.length = shard;
+ parity_header.cmpLength = shard;
+ parity_header.crc64 = aaruf_crc64_data(parity_data, shard);
+ parity_header.cmpCrc64 = parity_header.crc64;
+
+ /* Try compression */
+ uint8_t *cmp_buf = NULL;
+ size_t cmp_size = 0;
+
+ if(ctx->compression_enabled)
+ {
+ cmp_buf = (uint8_t *)malloc((size_t)shard * 2);
+ if(cmp_buf)
+ {
+ if(ctx->use_zstd)
+ {
+ cmp_size = aaruf_zstd_encode_buffer(cmp_buf, (size_t)shard * 2, parity_data, shard,
+ ctx->zstd_level, ctx->num_threads);
+ if(cmp_size > 0 && cmp_size < shard)
+ {
+ parity_header.compression = kCompressionZstd;
+ parity_header.cmpLength = (uint32_t)cmp_size;
+ parity_header.cmpCrc64 = aaruf_crc64_data(cmp_buf, (uint32_t)cmp_size);
+ ctx->has_zstd_blocks = true;
+ }
+ else
+ {
+ free(cmp_buf);
+ cmp_buf = NULL;
+ }
+ }
+ else
+ {
+ size_t dst_size = (size_t)shard * 2;
+ size_t props_size = LZMA_PROPERTIES_LENGTH;
+ uint8_t lzma_props[LZMA_PROPERTIES_LENGTH] = {0};
+ aaruf_lzma_encode_buffer(cmp_buf, &dst_size, parity_data, shard, lzma_props, &props_size, 9,
+ ctx->lzma_dict_size, 4, 0, 2, 273, LZMA_THREADS(ctx));
+ if(dst_size + LZMA_PROPERTIES_LENGTH < shard)
+ {
+ parity_header.compression = kCompressionLzma;
+ parity_header.cmpLength = (uint32_t)(dst_size + LZMA_PROPERTIES_LENGTH);
+ parity_header.cmpCrc64 = aaruf_crc64_data(cmp_buf, (uint32_t)dst_size);
+
+ /* Write: header + lzma_props + compressed data */
+ aaruf_fseek(ctx->imageStream, 0, SEEK_END);
+ uint64_t parity_offset = (uint64_t)aaruf_ftell(ctx->imageStream);
+ parity_offset = (parity_offset + alignment_mask) & ~alignment_mask;
+ aaruf_fseek(ctx->imageStream, (aaru_off_t)parity_offset, SEEK_SET);
+
+ fwrite(&parity_header, sizeof(BlockHeader), 1, ctx->imageStream);
+ fwrite(lzma_props, LZMA_PROPERTIES_LENGTH, 1, ctx->imageStream);
+ fwrite(cmp_buf, dst_size, 1, ctx->imageStream);
+ free(cmp_buf);
+
+ /* Record parity offset in descriptor */
+ StripeParityBlockEntry pentry;
+ pentry.offset = parity_offset;
+ memcpy(p, &pentry, sizeof(StripeParityBlockEntry)); p += sizeof(StripeParityBlockEntry);
+
+ /* Add index entry */
+ IndexEntry ie = {.blockType = DataBlock, .dataType = kDataTypeErasureParity,
+ .offset = parity_offset};
+ utarray_push_back(ctx->index_entries, &ie);
+ ctx->dirty_index_block = true;
+
+ /* Update next_block_position */
+ uint64_t total = sizeof(BlockHeader) + parity_header.cmpLength;
+ ctx->next_block_position = (parity_offset + total + alignment_mask) & ~alignment_mask;
+
+ /* Reset parity buffer */
+ memset(parity_data, 0, shard);
+ continue;
+ }
+ else
+ {
+ free(cmp_buf);
+ cmp_buf = NULL;
+ }
+ }
+ }
+ }
+
+ /* Write uncompressed (or compressed non-LZMA) parity */
+ aaruf_fseek(ctx->imageStream, 0, SEEK_END);
+ uint64_t parity_offset = (uint64_t)aaruf_ftell(ctx->imageStream);
+ parity_offset = (parity_offset + alignment_mask) & ~alignment_mask;
+ aaruf_fseek(ctx->imageStream, (aaru_off_t)parity_offset, SEEK_SET);
+
+ fwrite(&parity_header, sizeof(BlockHeader), 1, ctx->imageStream);
+ if(cmp_buf)
+ {
+ fwrite(cmp_buf, cmp_size, 1, ctx->imageStream);
+ free(cmp_buf);
+ }
+ else
+ {
+ fwrite(parity_data, shard, 1, ctx->imageStream);
+ }
+
+ /* Record parity offset in descriptor */
+ StripeParityBlockEntry pentry;
+ pentry.offset = parity_offset;
+ memcpy(p, &pentry, sizeof(StripeParityBlockEntry)); p += sizeof(StripeParityBlockEntry);
+
+ /* Add index entry */
+ IndexEntry ie = {.blockType = DataBlock, .dataType = kDataTypeErasureParity, .offset = parity_offset};
+ utarray_push_back(ctx->index_entries, &ie);
+ ctx->dirty_index_block = true;
+
+ /* Update next_block_position */
+ uint64_t total = sizeof(BlockHeader) + parity_header.cmpLength;
+ ctx->next_block_position = (parity_offset + total + alignment_mask) & ~alignment_mask;
+
+ /* Reset parity buffer */
+ memset(parity_data, 0, shard);
+ }
+
+ /* Store the completed stripe descriptor */
+ for(size_t i = 0; i < desc_data_size; i++)
+ utarray_push_back(ctx->ec_data_stripes, &desc[i]);
+
+ free(desc);
+
+ /* Reset stripe slot tracking */
+ for(uint16_t k = 0; k < K; k++)
+ {
+ size_t idx = (size_t)slot * K + k;
+ ctx->ec_data_block_offsets[idx] = 0;
+ ctx->ec_data_block_sizes[idx] = 0;
+ ctx->ec_data_shard_crcs[idx] = 0;
+ }
+ ctx->ec_data_stripe_counts[slot] = 0;
+}
+
+/**
+ * @brief Flush all partial data stripes and write the ECMB + recovery footer.
+ *
+ * Called from aaruf_finalize_write() after all metadata/DDT/index blocks are written.
+ *
+ * @param ctx Context with EC enabled.
+ */
+void ec_finalize(aaruformat_context *ctx)
+{
+ if(!ctx->ec_enabled) return;
+
+ const uint16_t K = ctx->ec_K;
+
+ /* Flush any partial stripe slots */
+ for(uint16_t slot = 0; slot < K; slot++)
+ {
+ if(ctx->ec_data_stripe_counts[slot] > 0)
+ ec_flush_data_stripe(ctx, slot);
+ }
+
+ /* Count completed stripes */
+ uint32_t stripe_count = 0;
+ /* Walk the flat stripe descriptor array to count entries.
+ * Each stripe starts with a uint16_t actualK, followed by actualK × 20-byte entries + M × 8-byte entries.
+ * Since we appended byte-by-byte, we need to parse. */
+ {
+ size_t total_bytes = utarray_len(ctx->ec_data_stripes);
+ uint8_t *base = (uint8_t *)utarray_front(ctx->ec_data_stripes);
+ size_t pos = 0;
+ while(base && pos + sizeof(uint16_t) <= total_bytes)
+ {
+ uint16_t ak;
+ memcpy(&ak, base + pos, sizeof(uint16_t));
+ pos += sizeof(uint16_t);
+ pos += (size_t)ak * sizeof(StripeDataBlockEntry);
+ pos += (size_t)ctx->ec_M * sizeof(StripeParityBlockEntry);
+ stripe_count++;
+ }
+ }
+
+ /* Build ECMB payload: 1 stripe group (data only for now) */
+ StripeGroupDescriptor group;
+ memset(&group, 0, sizeof(group));
+ group.groupType = kECGroupData;
+ group.K = ctx->ec_K;
+ group.M = ctx->ec_M;
+ group.shardSize = ctx->ec_data_shard_size;
+ group.stripeCount = stripe_count;
+ group.interleaveDepth = ctx->ec_K; /* full interleave */
+
+ size_t stripe_data_len = utarray_len(ctx->ec_data_stripes);
+ size_t payload_len = sizeof(StripeGroupDescriptor) + stripe_data_len;
+ uint8_t *payload = (uint8_t *)malloc(payload_len);
+ if(!payload) return;
+
+ memcpy(payload, &group, sizeof(StripeGroupDescriptor));
+ if(stripe_data_len > 0)
+ {
+ uint8_t *base = (uint8_t *)utarray_front(ctx->ec_data_stripes);
+ if(base) memcpy(payload + sizeof(StripeGroupDescriptor), base, stripe_data_len);
+ }
+
+ /* CRC64 of payload */
+ uint64_t payload_crc = aaruf_crc64_data(payload, (uint32_t)payload_len);
+
+ /* Build ECMB header */
+ ErasureCodingMapHeader ecmb;
+ memset(&ecmb, 0, sizeof(ecmb));
+ ecmb.identifier = ErasureCodingMapBlock;
+ ecmb.algorithm = ctx->ec_algorithm;
+ ecmb.stripeGroupCount = 1;
+ ecmb.compression = kCompressionNone;
+ ecmb.length = payload_len;
+ ecmb.cmpLength = payload_len;
+ ecmb.crc64 = payload_crc;
+ ecmb.cmpCrc64 = payload_crc;
+
+ /* Write ECMB to file */
+ uint64_t alignment_mask = (1ULL << ctx->user_data_ddt_header.blockAlignmentShift) - 1;
+ aaruf_fseek(ctx->imageStream, 0, SEEK_END);
+ uint64_t ecmb_offset = (uint64_t)aaruf_ftell(ctx->imageStream);
+ ecmb_offset = (ecmb_offset + alignment_mask) & ~alignment_mask;
+ aaruf_fseek(ctx->imageStream, (aaru_off_t)ecmb_offset, SEEK_SET);
+
+ fwrite(&ecmb, sizeof(ErasureCodingMapHeader), 1, ctx->imageStream);
+ fwrite(payload, payload_len, 1, ctx->imageStream);
+
+ uint64_t ecmb_total = sizeof(ErasureCodingMapHeader) + payload_len;
+
+ /* Write duplicate ECMB */
+ uint64_t ecmb2_offset = (ecmb_offset + ecmb_total + alignment_mask) & ~alignment_mask;
+ aaruf_fseek(ctx->imageStream, (aaru_off_t)ecmb2_offset, SEEK_SET);
+ fwrite(&ecmb, sizeof(ErasureCodingMapHeader), 1, ctx->imageStream);
+ fwrite(payload, payload_len, 1, ctx->imageStream);
+
+ free(payload);
+
+ /* Write recovery footer */
+ AaruRecoveryFooter footer;
+ memset(&footer, 0, sizeof(footer));
+ footer.ecmbOffset = ecmb_offset;
+ footer.ecmbLength = ecmb_total;
+ footer.headerCrc64 = aaruf_crc64_data((const uint8_t *)&ctx->header, sizeof(AaruHeaderV2));
+ memcpy(&footer.backupHeader, &ctx->header, sizeof(AaruHeaderV2));
+ footer.footerMagic = AARU_RECOVERY_FOOTER_MAGIC;
+
+ aaruf_fseek(ctx->imageStream, 0, SEEK_END);
+ fwrite(&footer, sizeof(AaruRecoveryFooter), 1, ctx->imageStream);
+
+ TRACE("Wrote ECMB at offset %" PRIu64 " (%" PRIu64 " bytes), footer at EOF", ecmb_offset, ecmb_total);
+}
+
+/**
+ * @brief Free all erasure coding state from the context.
+ *
+ * Called from aaruf_close().
+ *
+ * @param ctx Context.
+ */
+void ec_free(aaruformat_context *ctx)
+{
+ if(!ctx->ec_enabled) return;
+
+ if(ctx->ec_rs_ctx)
+ {
+ rs_free((rs_context *)ctx->ec_rs_ctx);
+ ctx->ec_rs_ctx = NULL;
+ }
+
+ if(ctx->ec_data_parity)
+ {
+ for(uint32_t i = 0; i < (uint32_t)ctx->ec_K * ctx->ec_M; i++)
+ free(ctx->ec_data_parity[i]);
+ free(ctx->ec_data_parity);
+ ctx->ec_data_parity = NULL;
+ }
+
+ free(ctx->ec_data_block_offsets); ctx->ec_data_block_offsets = NULL;
+ free(ctx->ec_data_block_sizes); ctx->ec_data_block_sizes = NULL;
+ free(ctx->ec_data_shard_crcs); ctx->ec_data_shard_crcs = NULL;
+ free(ctx->ec_data_stripe_counts); ctx->ec_data_stripe_counts = NULL;
+
+ if(ctx->ec_data_stripes)
+ {
+ utarray_free(ctx->ec_data_stripes);
+ ctx->ec_data_stripes = NULL;
+ }
+
+ ctx->ec_enabled = false;
+}
diff --git a/src/write.c b/src/write.c
index c05bd38..8a0e7a4 100644
--- a/src/write.c
+++ b/src/write.c
@@ -24,6 +24,7 @@
#include
#include "aaruformat.h"
+#include "erasure_internal.h"
#include "internal.h"
#include "log.h"
#include "ps3/ps3_crypto.h"
@@ -731,7 +732,7 @@ AARU_EXPORT int32_t AARU_CALL aaruf_write_sector_long(void *context, uint64_t se
memcpy(ctx->sector_ied + corrected_sector_address * 2, data + 4, 2);
memcpy(ctx->sector_cpr_mai + corrected_sector_address * 6, data + 2054, 6);
memcpy(ctx->sector_edc + corrected_sector_address * 4, data + 2060, 4);
-
+
return aaruf_write_sector(context, sector_address, negative, data + 6, sector_status, 2048);
}
@@ -1696,10 +1697,45 @@ int32_t aaruf_close_current_block(aaruformat_context *ctx)
free(cmp_buffer);
return AARUF_ERROR_CANNOT_WRITE_BLOCK_DATA;
}
-
- free(cmp_buffer);
}
+ /* --- Erasure coding parity accumulation ---
+ * Call BEFORE freeing cmp_buffer / writing_buffer.
+ * For LZMA: cmpLength already includes LZMA_PROPERTIES_LENGTH, but the properties
+ * were written separately. We pass them as lzma_props parameter.
+ * For non-LZMA compressed: cmp_buffer holds all payload.
+ * For uncompressed: writing_buffer holds payload. */
+ if(ctx->ec_enabled)
+ {
+ const uint8_t *ec_payload;
+ uint32_t ec_payload_size;
+ const uint8_t *ec_lzma_props = NULL;
+
+ if(ctx->current_block_header.compression == kCompressionNone)
+ {
+ ec_payload = ctx->writing_buffer;
+ ec_payload_size = ctx->current_block_header.length;
+ }
+ else if(ctx->current_block_header.compression == kCompressionLzma)
+ {
+ ec_payload = cmp_buffer;
+ ec_payload_size = ctx->current_block_header.cmpLength - LZMA_PROPERTIES_LENGTH;
+ ec_lzma_props = lzma_properties;
+ }
+ else
+ {
+ ec_payload = cmp_buffer;
+ ec_payload_size = ctx->current_block_header.cmpLength;
+ }
+
+ ec_accumulate_data_block(ctx, &ctx->current_block_header, ec_lzma_props,
+ ec_payload, ec_payload_size, index_entry.offset);
+ }
+
+ /* Free compressed buffer (if compression was used) */
+ if(ctx->current_block_header.compression != kCompressionNone)
+ free(cmp_buffer);
+
// Update nextBlockPosition to point to the next available aligned position
const uint64_t block_total_size = sizeof(BlockHeader) + ctx->current_block_header.cmpLength;
const uint64_t alignment_mask = (1ULL << ctx->user_data_ddt_header.blockAlignmentShift) - 1;