From 71f1ab529076f07ddde00e14d1341b4ca2c26b60 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 12 Apr 2026 15:09:38 +0200 Subject: [PATCH] Actually write the BD sector tags to file --- include/aaruformat/context.h | 3 +- src/close_write.c | 215 +++++++++++++++++++++++++++++++++++ src/create.c | 1 + src/write.c | 29 +++++ 4 files changed, 247 insertions(+), 1 deletion(-) diff --git a/include/aaruformat/context.h b/include/aaruformat/context.h index 0ed16e9..5b52c2b 100644 --- a/include/aaruformat/context.h +++ b/include/aaruformat/context.h @@ -208,7 +208,7 @@ typedef struct aaruformat_context uint8_t *sector_id; ///< DVD sector ID (4 bytes) if present. uint8_t *sector_ied; ///< DVD sector IED (2 bytes) if present. uint8_t *sector_cpr_mai; ///< DVD sector CPR_MAI (6 bytes) if present. - uint8_t *sector_edc; ///< DVD sector EDC (4 bytes) if present. + uint8_t *sector_edc; ///< DVD or Blu-ray sector EDC (4 bytes) if present (see DvdSectorEdc / BdSectorEdc data types). uint8_t *sector_decrypted_title_key; ///< DVD decrypted title key (5 bytes) if present. /* Metadata & geometry */ @@ -331,6 +331,7 @@ typedef struct aaruformat_context bool dirty_sector_suffix_ddt; ///< True if sector suffix DDT should be written during close bool dirty_sector_subchannel_block; ///< True if subchannel block should be written during close bool dirty_dvd_long_sector_blocks; ///< True if DVD long sector blocks should be written during close + bool dirty_bd_sector_edc_block; ///< True if Blu-ray sector EDC data block should be written during close bool dirty_dvd_title_key_decrypted_block; ///< True if decrypted title key block should be written during close bool dirty_media_tags; ///< True if media tags should be written during close bool dirty_tape_ddt; ///< True if tape DDT should be written during close diff --git a/src/close_write.c b/src/close_write.c index 239fc22..c5df5f4 100644 --- a/src/close_write.c +++ b/src/close_write.c @@ -2566,6 +2566,218 @@ static void write_dvd_long_sector_blocks(aaruformat_context *ctx) if(edc_block.compression != kCompressionNone) free(buffer); } +/** + * @brief Serialize Blu-ray long-sector EDC (4 bytes per sector) to the image file. + * + * Blu-ray user sectors are normally 2048 bytes; long BD sectors supplied to the library are + * 2052 bytes, with 4 bytes of EDC immediately following the user data. That EDC is stored in + * ctx->sector_edc (one uint32_t per logical sector) while the main user data path stores only + * 2048-byte sectors in the DDT. This function writes those EDC bytes as a single DataBlock so + * they round-trip on open and through aaruf_read_sector_long(). + * + * This is separate from write_dvd_long_sector_blocks(): DVD long sectors require four auxiliary + * buffers (ID, IED, CPR/MAI, EDC) and are written only when all four are present; BD imaging + * only needs the EDC buffer. The on-disk layout of the payload matches the DVD EDC block (raw + * concatenation, 4 bytes per sector), but the block type is kDataTypeBdSectorEdc so readers + * distinguish BD from DVD auxiliary data. + * + * **Single auxiliary data block written:** + * + * 1. **Blu-ray Sector EDC Block (BdSectorEdc)**: 4 bytes per sector + * - Contains the EDC field from long BD sectors (bytes 2048–2051 of each long sector) + * - Used for error detection over the user payload; preserved for forensic and archival use + * + * **Block structure:** + * The block consists of: + * 1. BlockHeader with identifier DataBlock, type kDataTypeBdSectorEdc, compression (None, + * Zstd, or Lzma when enabled), uncompressed and compressed lengths, and CRC64 checksums + * 2. Optional LZMA property bytes when compression is LZMA + * 3. Raw auxiliary data: EDC fields for all logical sectors (negative, normal, overflow), + * possibly stored compressed + * + * The total number of sectors matches the user-data DDT span: + * total_sectors = negative + Sectors + overflow + * + * **Write sequence:** + * 1. Seek to end of file + * 2. Align file position to block boundary (using blockAlignmentShift) + * 3. Construct BlockHeader with type kDataTypeBdSectorEdc and length = total_sectors * 4 + * 4. Calculate CRC64 over the uncompressed auxiliary buffer + * 5. Optionally compress; if compression does not shrink the payload, store uncompressed + * 6. Write BlockHeader (sizeof(BlockHeader) bytes), optional LZMA props, then payload + * 7. On success, replace any existing index entry for this block type and append a new IndexEntry + * + * **Alignment and file positioning:** + * The file position is moved to EOF then aligned forward so that + * (position & alignment_mask) == 0, with alignment_mask from ctx->user_data_ddt_header.blockAlignmentShift, + * matching other DataBlocks in the image. + * + * **Index registration:** + * After a successful write, any prior DataBlock index entry with dataType kDataTypeBdSectorEdc + * is removed, then a new entry is pushed with offset equal to the aligned position of the + * BlockHeader. ctx->dirty_index_block is set to true. + * + * **Error handling:** + * Write failures (fwrite returning other than 1) are not propagated; no index update occurs on + * failure. TRACE logs record success paths. Compression allocation failure returns early + * without writing. + * + * **No-op conditions:** + * If ctx->sector_edc is NULL, the function returns immediately without writing, allowing BD + * images created without long-sector EDC to close normally. + * + * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. + * ctx->sector_edc holds 4 * total_sectors bytes when present (may be NULL). + * ctx->imageStream must be open and writable. ctx->index_entries must be initialized + * (utarray) to accept new index entries. + * + * @note BD long sector format: + * - User data remains 2048 bytes per sector in the main data path + * - Long sectors are 2052 bytes: 2048 bytes user + 4 bytes EDC + * - EDC is stored separately under kDataTypeBdSectorEdc for parity with the format spec + * + * @note Sector coverage: + * - Negative sectors: lead-in area before sector 0 (if present) + * - Normal sectors: main data area (0 to Sectors-1) + * - Overflow sectors: lead-out or beyond user extent (if present) + * - The buffer is indexed by the same corrected sector index as DVD auxiliary data + * + * @note Memory management: + * - Temporary buffers may be allocated for compression; freed before return when used + * - ctx->sector_edc is owned by the context and freed on aaruf_close() + * + * @note Order in close sequence: + * - Invoked from aaruf_finalize_write() when dirty_bd_sector_edc_block is set, adjacent + * to write_dvd_long_sector_blocks() for optical auxiliary blocks + * + * @warning The sector_edc buffer must cover all total_sectors entries; sparse or partial buffers + * produce incorrect images. + * + * @warning When compression is enabled, behavior matches other DataBlocks (Zstd or LZMA); + * uncompressed storage is used if compression expands the payload. + * + * @see write_dvd_long_sector_blocks() for the four-block DVD auxiliary path. + * @see aaruf_write_sector() for the 2052-byte BD branch that populates sector_edc. + * @see BlockHeader for the block header structure definition. + * + * @internal + */ +static void write_bd_sector_edc_block(aaruformat_context *ctx) +{ + if(ctx->sector_edc == NULL) return; + + uint64_t total_sectors = + ctx->user_data_ddt_header.negative + ctx->image_info.Sectors + ctx->user_data_ddt_header.overflow; + + const uint64_t alignment_mask = (1ULL << ctx->user_data_ddt_header.blockAlignmentShift) - 1; + + aaruf_fseek(ctx->imageStream, 0, SEEK_END); + aaru_off_t edc_position = aaruf_ftell(ctx->imageStream); + if(edc_position & alignment_mask) + { + const uint64_t aligned_position = edc_position + alignment_mask & ~alignment_mask; + aaruf_fseek(ctx->imageStream, aligned_position, SEEK_SET); + edc_position = aligned_position; + } + TRACE("Writing BD sector EDC block at position %ld", edc_position); + BlockHeader edc_block = {0}; + edc_block.identifier = DataBlock; + edc_block.type = kDataTypeBdSectorEdc; + edc_block.compression = + ctx->compression_enabled ? (ctx->use_zstd ? kCompressionZstd : kCompressionLzma) : kCompressionNone; + edc_block.length = (uint32_t)total_sectors * 4; + edc_block.crc64 = aaruf_crc64_data(ctx->sector_edc, edc_block.length); + + uint8_t *buffer = NULL; + uint8_t lzma_properties[LZMA_PROPERTIES_LENGTH] = {0}; + + if(edc_block.compression == kCompressionNone) + { + buffer = ctx->sector_edc; + edc_block.cmpCrc64 = edc_block.crc64; + } + else + { + buffer = malloc((size_t)edc_block.length * 2); + if(buffer == NULL) + { + TRACE("Failed to allocate memory for BD sector EDC compression"); + return; + } + + size_t dst_size; + + if(ctx->use_zstd) + { + dst_size = aaruf_zstd_encode_buffer(buffer, (size_t)edc_block.length * 2, ctx->sector_edc, + edc_block.length, ctx->zstd_level, ctx->num_threads); + if(dst_size == 0) dst_size = edc_block.length; + } + else + { + dst_size = (size_t)edc_block.length * 2 * 2; + size_t props_size = LZMA_PROPERTIES_LENGTH; + aaruf_lzma_encode_buffer(buffer, &dst_size, ctx->sector_edc, edc_block.length, lzma_properties, &props_size, + 9, ctx->lzma_dict_size, 4, 0, 2, 273, LZMA_THREADS(ctx)); + } + + edc_block.cmpLength = (uint32_t)dst_size; + + if(edc_block.cmpLength >= edc_block.length) + { + edc_block.compression = kCompressionNone; + free(buffer); + buffer = ctx->sector_edc; + } + } + + if(edc_block.compression == kCompressionNone) + { + edc_block.cmpLength = edc_block.length; + edc_block.cmpCrc64 = edc_block.crc64; + } + else + { + edc_block.cmpCrc64 = aaruf_crc64_data(buffer, edc_block.cmpLength); + if(ctx->use_zstd) ctx->has_zstd_blocks = true; + } + + size_t length_to_write = edc_block.cmpLength; + if(edc_block.compression == kCompressionLzma) edc_block.cmpLength += LZMA_PROPERTIES_LENGTH; + + if(fwrite(&edc_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) + { + if(edc_block.compression == kCompressionLzma) + fwrite(lzma_properties, LZMA_PROPERTIES_LENGTH, 1, ctx->imageStream); + + const size_t written_bytes = fwrite(buffer, length_to_write, 1, ctx->imageStream); + if(written_bytes == 1) + { + TRACE("Successfully wrote BD sector EDC block (%" PRIu64 " bytes)", edc_block.cmpLength); + + for(int k = utarray_len(ctx->index_entries) - 1; k >= 0; k--) + { + const IndexEntry *entry = (IndexEntry *)utarray_eltptr(ctx->index_entries, k); + if(entry && entry->blockType == DataBlock && entry->dataType == kDataTypeBdSectorEdc) + { + TRACE("Found existing BD sector EDC block index entry at position %d, removing", k); + utarray_erase(ctx->index_entries, k, 1); + } + } + + IndexEntry edc_index_entry; + edc_index_entry.blockType = DataBlock; + edc_index_entry.dataType = kDataTypeBdSectorEdc; + edc_index_entry.offset = edc_position; + utarray_push_back(ctx->index_entries, &edc_index_entry); + ctx->dirty_index_block = true; + TRACE("Added BD sector EDC block index entry at offset %" PRIu64, edc_position); + } + } + + if(edc_block.compression != kCompressionNone) free(buffer); +} + /** * @brief Serialize the DVD decrypted title key data block to the image file. * @@ -5350,6 +5562,9 @@ int32_t aaruf_finalize_write(aaruformat_context *ctx) // Write DVD long sector data blocks if(ctx->dirty_dvd_long_sector_blocks) write_dvd_long_sector_blocks(ctx); + // Write Blu-ray sector EDC block (long sectors) + if(ctx->dirty_bd_sector_edc_block) write_bd_sector_edc_block(ctx); + // Write DVD decrypted title keys if(ctx->dirty_dvd_title_key_decrypted_block) write_dvd_title_key_decrypted_block(ctx); diff --git a/src/create.c b/src/create.c index 090f564..080023f 100644 --- a/src/create.c +++ b/src/create.c @@ -594,6 +594,7 @@ AARU_EXPORT void *AARU_CALL aaruf_create(const char *filepath, const uint32_t me ctx->dirty_sector_suffix_ddt = true; ctx->dirty_sector_subchannel_block = true; ctx->dirty_dvd_long_sector_blocks = true; + ctx->dirty_bd_sector_edc_block = true; ctx->dirty_dvd_title_key_decrypted_block = true; ctx->dirty_media_tags = true; ctx->dirty_tape_ddt = true; diff --git a/src/write.c b/src/write.c index 8a0e7a4..257048f 100644 --- a/src/write.c +++ b/src/write.c @@ -2681,6 +2681,35 @@ AARU_EXPORT int32_t AARU_CALL aaruf_write_sector_tag(void *context, const uint64 ctx->dirty_sector_subchannel_block = true; // Mark subchannel block as dirty TRACE("Exiting aaruf_write_sector_tag() = AARUF_STATUS_OK"); return AARUF_STATUS_OK; + case kSectorTagBdSectorEdc: + if(ctx->image_info.MetadataMediaType != BlockMedia) + { + FATAL("Invalid media type for tag"); + TRACE("Exiting aaruf_write_sector_tag() = AARUF_ERROR_INCORRECT_MEDIA_TYPE"); + return AARUF_ERROR_INCORRECT_MEDIA_TYPE; + } + + if(length != 4) + { + FATAL("Incorrect tag size"); + TRACE("Exiting aaruf_write_sector_tag() = AARUF_ERROR_INCORRECT_DATA_SIZE"); + return AARUF_ERROR_INCORRECT_DATA_SIZE; + } + + if(ctx->sector_edc == NULL) ctx->sector_edc = calloc(1, 4 * total_sectors); + + if(ctx->sector_edc == NULL) + { + FATAL("Could not allocate memory for Blu-ray sector EDC"); + + TRACE("Exiting aaruf_write_sector_tag() = AARUF_ERROR_NOT_ENOUGH_MEMORY"); + return AARUF_ERROR_NOT_ENOUGH_MEMORY; + } + + memcpy(ctx->sector_edc + corrected_sector_address * 4, data, 4); + ctx->dirty_bd_sector_edc_block = true; // Mark BD sector EDC block as dirty + TRACE("Exiting aaruf_write_sector_tag() = AARUF_STATUS_OK"); + return AARUF_STATUS_OK; default: TRACE("Do not know how to write sector tag %d", tag); return AARUF_ERROR_INVALID_TAG;