/* * This file is part of the Aaru Data Preservation Suite. * Copyright (c) 2019-2025 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 close.c * @brief Implements image finalization and resource cleanup for libaaruformat. * * This translation unit contains the logic that flushes any remaining in-memory * structures (deduplication tables, checksum information, track metadata, MODE 2 * subheaders, sector prefix data and the global index) to the on-disk Aaru image * when closing a context opened for writing. It also performs orderly teardown of * dynamically allocated resources regardless of read or write mode. * * Helper (static) functions perform discrete serialization steps; the public * entry point is ::aaruf_close(). All write helpers assume that the caller has * already validated the context magic and (for write mode) written the initial * provisional header at offset 0. Functions return libaaruformat status codes * (AARUF_STATUS_OK on success or an AARUF_ERROR_* constant) except for * ::aaruf_close(), which returns 0 on success and -1 on failure while setting * errno. */ #include #include #include #ifdef __linux__ #include #endif #include #include "internal.h" #include "log.h" /** * @brief Flush a cached secondary (child) DeDuplication Table (DDT) to the image. * * When working with a multi-level DDT (i.e. primary table with tableShift > 0), a single * secondary table may be cached in memory while sectors belonging to its range are written. * This function serializes the currently cached secondary table (if any) at the end of the * file, aligning the write position to the DDT block alignment, and updates the corresponding * primary table entry to point to the new block-aligned location. The primary table itself is * then re-written in-place (only its data array portion) to persist the updated pointer. The * index is updated by removing any previous index entry for the same secondary table offset * and inserting a new one for the freshly written table. * * CRC64 is computed for the serialized table contents and stored in both crc64 and cmpCrc64 * fields of the written DdtHeader2 (no compression is applied). * * On return the cached secondary table buffers and bookkeeping fields (cachedSecondaryDdtSmall, * cachedSecondaryDdtBig, cachedDdtOffset) are cleared. * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @return AARUF_STATUS_OK on success, or AARUF_ERROR_CANNOT_WRITE_HEADER if the * secondary table or updated primary table cannot be flushed. * @retval AARUF_STATUS_OK Success or no cached secondary DDT needed flushing. * @retval AARUF_ERROR_CANNOT_WRITE_HEADER Failed writing secondary table or rewriting primary table. * @note If no cached secondary DDT is pending (detected via tableShift and cache pointers), * the function is a no-op returning AARUF_STATUS_OK. * @internal */ static int32_t write_cached_secondary_ddt(aaruformatContext *ctx) { // Write cached secondary table to file end and update primary table entry with its position // Check if we have a cached table that needs to be written (either it has an offset or exists in memory) bool has_cached_secondary_ddt = ctx->userDataDdtHeader.tableShift > 0 && (ctx->cachedDdtOffset != 0 || ctx->cachedSecondaryDdtSmall != NULL || ctx->cachedSecondaryDdtBig != NULL); if(!has_cached_secondary_ddt) return AARUF_STATUS_OK; TRACE("Writing cached secondary DDT table to file"); fseek(ctx->imageStream, 0, SEEK_END); long end_of_file = ftell(ctx->imageStream); // Align the position according to block alignment shift uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(end_of_file & alignment_mask) { // Calculate the next aligned position uint64_t aligned_position = end_of_file + alignment_mask & ~alignment_mask; // Seek to the aligned position and pad with zeros if necessary fseek(ctx->imageStream, aligned_position, SEEK_SET); end_of_file = aligned_position; TRACE("Aligned DDT write position from %ld to %" PRIu64 " (alignment shift: %d)", ftell(ctx->imageStream) - (aligned_position - end_of_file), aligned_position, ctx->userDataDdtHeader.blockAlignmentShift); } // Prepare DDT header for the cached table DdtHeader2 ddt_header = {0}; ddt_header.identifier = DeDuplicationTable2; ddt_header.type = UserData; ddt_header.compression = None; ddt_header.levels = ctx->userDataDdtHeader.levels; ddt_header.tableLevel = ctx->userDataDdtHeader.tableLevel + 1; ddt_header.previousLevelOffset = ctx->primaryDdtOffset; ddt_header.negative = ctx->userDataDdtHeader.negative; ddt_header.overflow = ctx->userDataDdtHeader.overflow; ddt_header.blockAlignmentShift = ctx->userDataDdtHeader.blockAlignmentShift; ddt_header.dataShift = ctx->userDataDdtHeader.dataShift; ddt_header.tableShift = 0; // Secondary tables are single level ddt_header.sizeType = ctx->userDataDdtHeader.sizeType; uint64_t items_per_ddt_entry = 1 << ctx->userDataDdtHeader.tableShift; ddt_header.blocks = items_per_ddt_entry; ddt_header.entries = items_per_ddt_entry; ddt_header.start = ctx->cachedDdtPosition * items_per_ddt_entry; // Calculate data size if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) ddt_header.length = items_per_ddt_entry * sizeof(uint16_t); else ddt_header.length = items_per_ddt_entry * sizeof(uint32_t); ddt_header.cmpLength = ddt_header.length; // Calculate CRC64 of the data crc64_ctx *crc64_context = aaruf_crc64_init(); if(crc64_context != NULL) { if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) aaruf_crc64_update(crc64_context, (uint8_t *)ctx->cachedSecondaryDdtSmall, ddt_header.length); else aaruf_crc64_update(crc64_context, (uint8_t *)ctx->cachedSecondaryDdtBig, ddt_header.length); uint64_t crc64; aaruf_crc64_final(crc64_context, &crc64); ddt_header.crc64 = crc64; ddt_header.cmpCrc64 = crc64; } // Write header if(fwrite(&ddt_header, sizeof(DdtHeader2), 1, ctx->imageStream) == 1) { // Write data size_t written_bytes = 0; if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) written_bytes = fwrite(ctx->cachedSecondaryDdtSmall, ddt_header.length, 1, ctx->imageStream); else written_bytes = fwrite(ctx->cachedSecondaryDdtBig, ddt_header.length, 1, ctx->imageStream); if(written_bytes == 1) { // Update primary table entry to point to new location uint64_t new_secondary_table_block_offset = end_of_file >> ctx->userDataDdtHeader.blockAlignmentShift; if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) ctx->userDataDdtMini[ctx->cachedDdtPosition] = (uint16_t)new_secondary_table_block_offset; else ctx->userDataDdtBig[ctx->cachedDdtPosition] = (uint32_t)new_secondary_table_block_offset; // Update index: remove old entry for cached DDT and add new one TRACE("Updating index for cached secondary DDT"); // Remove old index entry for the cached DDT if(ctx->cachedDdtOffset != 0) { TRACE("Removing old index entry for DDT at offset %" PRIu64, ctx->cachedDdtOffset); IndexEntry *entry = NULL; // Find and remove the old index entry for(unsigned int k = 0; k < utarray_len(ctx->indexEntries); k++) { entry = (IndexEntry *)utarray_eltptr(ctx->indexEntries, k); if(entry && entry->offset == ctx->cachedDdtOffset && entry->blockType == DeDuplicationTable2) { TRACE("Found old DDT index entry at position %u, removing", k); utarray_erase(ctx->indexEntries, k, 1); break; } } } // Add new index entry for the newly written secondary DDT IndexEntry new_ddt_entry; new_ddt_entry.blockType = DeDuplicationTable2; new_ddt_entry.dataType = UserData; new_ddt_entry.offset = end_of_file; utarray_push_back(ctx->indexEntries, &new_ddt_entry); TRACE("Added new DDT index entry at offset %" PRIu64, end_of_file); // Write the updated primary table back to its original position in the file long saved_pos = ftell(ctx->imageStream); fseek(ctx->imageStream, ctx->primaryDdtOffset + sizeof(DdtHeader2), SEEK_SET); size_t primary_table_size = ctx->userDataDdtHeader.sizeType == SmallDdtSizeType ? ctx->userDataDdtHeader.entries * sizeof(uint16_t) : ctx->userDataDdtHeader.entries * sizeof(uint32_t); size_t primary_written_bytes = 0; if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) primary_written_bytes = fwrite(ctx->userDataDdtMini, primary_table_size, 1, ctx->imageStream); else primary_written_bytes = fwrite(ctx->userDataDdtBig, primary_table_size, 1, ctx->imageStream); if(primary_written_bytes != 1) { TRACE("Could not flush primary DDT table to file."); return AARUF_ERROR_CANNOT_WRITE_HEADER; } fseek(ctx->imageStream, saved_pos, SEEK_SET); } else TRACE("Failed to write cached secondary DDT data"); } else TRACE("Failed to write cached secondary DDT header"); // Free the cached table if(ctx->cachedSecondaryDdtSmall != NULL) { free(ctx->cachedSecondaryDdtSmall); ctx->cachedSecondaryDdtSmall = NULL; } if(ctx->cachedSecondaryDdtBig != NULL) { free(ctx->cachedSecondaryDdtBig); ctx->cachedSecondaryDdtBig = NULL; } ctx->cachedDdtOffset = 0; // Set position fseek(ctx->imageStream, 0, SEEK_END); return AARUF_STATUS_OK; } /** * @brief Write (flush) the multi-level primary DDT table header and data back to its file offset. * * This function is applicable only when a multi-level DDT is in use (tableShift > 0). It updates * the header fields (identifier, type, compression, CRC, lengths) and writes first the header and * then the entire primary table data block at ctx->primaryDdtOffset. The function also pushes an * IndexEntry for the primary DDT into the in-memory index array so that later write_index_block() * will serialize it. * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @return AARUF_STATUS_OK on success; AARUF_ERROR_CANNOT_WRITE_HEADER if either the header or the * table body cannot be written. Returns AARUF_STATUS_OK immediately if no primary table * should be written (single-level DDT or table buffers absent). * @retval AARUF_STATUS_OK Success or nothing to do (no multi-level primary table present). * @retval AARUF_ERROR_CANNOT_WRITE_HEADER Failed writing header or primary table data. * @internal */ static int32_t write_primary_ddt(aaruformatContext *ctx) { // Write the cached primary DDT table back to its position in the file if(ctx->userDataDdtHeader.tableShift <= 0 || ctx->userDataDdtMini == NULL && ctx->userDataDdtBig == NULL) return AARUF_STATUS_OK; TRACE("Writing cached primary DDT table back to file"); // Calculate CRC64 of the primary DDT table data first crc64_ctx *crc64_context = aaruf_crc64_init(); if(crc64_context != NULL) { size_t primary_table_size = ctx->userDataDdtHeader.sizeType == SmallDdtSizeType ? ctx->userDataDdtHeader.entries * sizeof(uint16_t) : ctx->userDataDdtHeader.entries * sizeof(uint32_t); if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) aaruf_crc64_update(crc64_context, (uint8_t *)ctx->userDataDdtMini, primary_table_size); else aaruf_crc64_update(crc64_context, (uint8_t *)ctx->userDataDdtBig, primary_table_size); uint64_t crc64; aaruf_crc64_final(crc64_context, &crc64); // Properly populate all header fields for multi-level DDT primary table ctx->userDataDdtHeader.identifier = DeDuplicationTable2; ctx->userDataDdtHeader.type = UserData; ctx->userDataDdtHeader.compression = None; // levels, tableLevel, previousLevelOffset, negative, overflow, blockAlignmentShift, // dataShift, tableShift, sizeType, entries, blocks, start are already set during creation ctx->userDataDdtHeader.crc64 = crc64; ctx->userDataDdtHeader.cmpCrc64 = crc64; ctx->userDataDdtHeader.length = primary_table_size; ctx->userDataDdtHeader.cmpLength = primary_table_size; TRACE("Calculated CRC64 for primary DDT: 0x%16lX", crc64); } // First write the DDT header fseek(ctx->imageStream, ctx->primaryDdtOffset, SEEK_SET); size_t headerWritten = fwrite(&ctx->userDataDdtHeader, sizeof(DdtHeader2), 1, ctx->imageStream); if(headerWritten != 1) { TRACE("Failed to write primary DDT header to file"); return AARUF_ERROR_CANNOT_WRITE_HEADER; } // Then write the table data (position is already after the header) size_t primary_table_size = ctx->userDataDdtHeader.sizeType == SmallDdtSizeType ? ctx->userDataDdtHeader.entries * sizeof(uint16_t) : ctx->userDataDdtHeader.entries * sizeof(uint32_t); // Write the primary table data size_t written_bytes = 0; if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) written_bytes = fwrite(ctx->userDataDdtMini, primary_table_size, 1, ctx->imageStream); else written_bytes = fwrite(ctx->userDataDdtBig, primary_table_size, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote primary DDT header and table to file (%" PRIu64 " entries, %zu bytes)", ctx->userDataDdtHeader.entries, primary_table_size); // Add primary DDT to index TRACE("Adding primary DDT to index"); IndexEntry primary_ddt_entry; primary_ddt_entry.blockType = DeDuplicationTable2; primary_ddt_entry.dataType = UserData; primary_ddt_entry.offset = ctx->primaryDdtOffset; utarray_push_back(ctx->indexEntries, &primary_ddt_entry); TRACE("Added primary DDT index entry at offset %" PRIu64, ctx->primaryDdtOffset); } else TRACE("Failed to write primary DDT table to file"); return AARUF_STATUS_OK; } /** * @brief Serialize a single-level DDT (tableShift == 0) directly after its header. * * For single-level DDT configurations the entire table of sector references is contiguous. * This routine computes a CRC64 for the table, populates all header metadata, writes the header * at ctx->primaryDdtOffset followed immediately by the table, and registers the block in the * index. * * @param ctx Pointer to an initialized aaruformatContext in write mode with tableShift == 0. * @return AARUF_STATUS_OK on success; AARUF_ERROR_CANNOT_WRITE_HEADER if serialization fails. * Returns AARUF_STATUS_OK without action when the context represents a multi-level DDT * or the table buffers are NULL. * @retval AARUF_STATUS_OK Success or nothing to do (not single-level / buffers missing). * @retval AARUF_ERROR_CANNOT_WRITE_HEADER Failed writing header or table data. * @internal */ static int32_t write_single_level_ddt(aaruformatContext *ctx) { // Write the single level DDT table block aligned just after the header if(ctx->userDataDdtHeader.tableShift != 0 || ctx->userDataDdtMini == NULL && ctx->userDataDdtBig == NULL) return AARUF_STATUS_OK; TRACE("Writing single-level DDT table to file"); // Calculate CRC64 of the primary DDT table data crc64_ctx *crc64_context = aaruf_crc64_init(); if(crc64_context != NULL) { size_t primary_table_size = ctx->userDataDdtHeader.sizeType == SmallDdtSizeType ? ctx->userDataDdtHeader.entries * sizeof(uint16_t) : ctx->userDataDdtHeader.entries * sizeof(uint32_t); if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) aaruf_crc64_update(crc64_context, (uint8_t *)ctx->userDataDdtMini, primary_table_size); else aaruf_crc64_update(crc64_context, (uint8_t *)ctx->userDataDdtBig, primary_table_size); uint64_t crc64; aaruf_crc64_final(crc64_context, &crc64); // Properly populate all header fields ctx->userDataDdtHeader.identifier = DeDuplicationTable2; ctx->userDataDdtHeader.type = UserData; ctx->userDataDdtHeader.compression = None; ctx->userDataDdtHeader.levels = 1; // Single level ctx->userDataDdtHeader.tableLevel = 0; // Top level ctx->userDataDdtHeader.previousLevelOffset = 0; // No previous level for single-level DDT // negative and overflow are already set during creation // blockAlignmentShift, dataShift, tableShift, sizeType, entries, blocks, start are already set ctx->userDataDdtHeader.crc64 = crc64; ctx->userDataDdtHeader.cmpCrc64 = crc64; ctx->userDataDdtHeader.length = primary_table_size; ctx->userDataDdtHeader.cmpLength = primary_table_size; TRACE("Calculated CRC64 for single-level DDT: 0x%16lX", crc64); } // Write the DDT header first fseek(ctx->imageStream, ctx->primaryDdtOffset, SEEK_SET); size_t header_written = fwrite(&ctx->userDataDdtHeader, sizeof(DdtHeader2), 1, ctx->imageStream); if(header_written != 1) { TRACE("Failed to write single-level DDT header to file"); return AARUF_ERROR_CANNOT_WRITE_HEADER; } // Then write the table data (position is already after the header) size_t primary_table_size = ctx->userDataDdtHeader.sizeType == SmallDdtSizeType ? ctx->userDataDdtHeader.entries * sizeof(uint16_t) : ctx->userDataDdtHeader.entries * sizeof(uint32_t); // Write the primary table data size_t written_bytes = 0; if(ctx->userDataDdtHeader.sizeType == SmallDdtSizeType) written_bytes = fwrite(ctx->userDataDdtMini, primary_table_size, 1, ctx->imageStream); else written_bytes = fwrite(ctx->userDataDdtBig, primary_table_size, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote single-level DDT header and table to file (%" PRIu64 " entries, %zu bytes)", ctx->userDataDdtHeader.entries, primary_table_size); // Add single-level DDT to index TRACE("Adding single-level DDT to index"); IndexEntry single_ddt_entry; single_ddt_entry.blockType = DeDuplicationTable2; single_ddt_entry.dataType = UserData; single_ddt_entry.offset = ctx->primaryDdtOffset; utarray_push_back(ctx->indexEntries, &single_ddt_entry); TRACE("Added single-level DDT index entry at offset %" PRIu64, ctx->primaryDdtOffset); } else TRACE("Failed to write single-level DDT table data to file"); return AARUF_STATUS_OK; } /** * @brief Finalize any active checksum calculations and append a checksum block. * * This routine completes pending hash contexts (MD5, SHA-1, SHA-256, SpamSum, BLAKE3), marks the * presence flags in ctx->checksums, and if at least one checksum exists writes a ChecksumBlock at * the end of the image (block-aligned). Individual ChecksumEntry records are serialized for each * available algorithm and the block is indexed. Feature flags (e.g. AARU_FEATURE_RW_BLAKE3) are * updated if required. * * Memory ownership: for SpamSum a buffer is allocated here if a digest was being computed and is * subsequently written without being freed inside this function (it will be freed during close). * The BLAKE3 context is freed after finalization. * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @internal */ static void write_checksum_block(aaruformatContext *ctx) { uint64_t alignment_mask; uint64_t aligned_position; // Finalize pending checksums if(ctx->calculating_md5) { ctx->checksums.hasMd5 = true; aaruf_md5_final(&ctx->md5_context, ctx->checksums.md5); } if(ctx->calculating_sha1) { ctx->checksums.hasSha1 = true; aaruf_sha1_final(&ctx->sha1_context, ctx->checksums.sha1); } if(ctx->calculating_sha256) { ctx->checksums.hasSha256 = true; aaruf_sha256_final(&ctx->sha256_context, ctx->checksums.sha256); } if(ctx->calculating_spamsum) { ctx->checksums.hasSpamSum = true; ctx->checksums.spamsum = calloc(1, FUZZY_MAX_RESULT); aaruf_spamsum_final(ctx->spamsum_context, ctx->checksums.spamsum); aaruf_spamsum_free(ctx->spamsum_context); } if(ctx->calculating_blake3) { ctx->checksums.hasBlake3 = true; blake3_hasher_finalize(ctx->blake3_context, ctx->checksums.blake3, BLAKE3_OUT_LEN); free(ctx->blake3_context); } // Write the checksums block bool has_checksums = ctx->checksums.hasMd5 || ctx->checksums.hasSha1 || ctx->checksums.hasSha256 || ctx->checksums.hasSpamSum || ctx->checksums.hasBlake3; if(!has_checksums) return; ChecksumHeader checksum_header = {0}; checksum_header.identifier = ChecksumBlock; fseek(ctx->imageStream, 0, SEEK_END); long checksum_position = ftell(ctx->imageStream); // Align index position to block boundary if needed alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(checksum_position & alignment_mask) { aligned_position = checksum_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); checksum_position = aligned_position; } // Skip checksum_header fseek(ctx->imageStream, sizeof(checksum_header), SEEK_CUR); if(ctx->checksums.hasMd5) { TRACE("Writing MD5 checksum entry"); ChecksumEntry md5_entry = {0}; md5_entry.length = MD5_DIGEST_LENGTH; md5_entry.type = Md5; fwrite(&md5_entry, sizeof(ChecksumEntry), 1, ctx->imageStream); fwrite(&ctx->checksums.md5, MD5_DIGEST_LENGTH, 1, ctx->imageStream); checksum_header.length += sizeof(ChecksumEntry) + MD5_DIGEST_LENGTH; checksum_header.entries++; } if(ctx->checksums.hasSha1) { TRACE("Writing SHA1 checksum entry"); ChecksumEntry sha1_entry = {0}; sha1_entry.length = SHA1_DIGEST_LENGTH; sha1_entry.type = Sha1; fwrite(&sha1_entry, sizeof(ChecksumEntry), 1, ctx->imageStream); fwrite(&ctx->checksums.sha1, SHA1_DIGEST_LENGTH, 1, ctx->imageStream); checksum_header.length += sizeof(ChecksumEntry) + SHA1_DIGEST_LENGTH; checksum_header.entries++; } if(ctx->checksums.hasSha256) { TRACE("Writing SHA256 checksum entry"); ChecksumEntry sha256_entry = {0}; sha256_entry.length = SHA256_DIGEST_LENGTH; sha256_entry.type = Sha256; fwrite(&sha256_entry, sizeof(ChecksumEntry), 1, ctx->imageStream); fwrite(&ctx->checksums.sha256, SHA256_DIGEST_LENGTH, 1, ctx->imageStream); checksum_header.length += sizeof(ChecksumEntry) + SHA256_DIGEST_LENGTH; checksum_header.entries++; } if(ctx->checksums.hasSpamSum) { TRACE("Writing SpamSum checksum entry"); ChecksumEntry spamsum_entry = {0}; spamsum_entry.length = strlen((const char *)ctx->checksums.spamsum); spamsum_entry.type = SpamSum; fwrite(&spamsum_entry, sizeof(ChecksumEntry), 1, ctx->imageStream); fwrite(ctx->checksums.spamsum, spamsum_entry.length, 1, ctx->imageStream); checksum_header.length += sizeof(ChecksumEntry) + spamsum_entry.length; checksum_header.entries++; } if(ctx->checksums.hasBlake3) { TRACE("Writing BLAKE3 checksum entry"); ChecksumEntry blake3_entry = {0}; blake3_entry.length = BLAKE3_OUT_LEN; blake3_entry.type = Blake3; fwrite(&blake3_entry, sizeof(ChecksumEntry), 1, ctx->imageStream); fwrite(&ctx->checksums.blake3, BLAKE3_OUT_LEN, 1, ctx->imageStream); checksum_header.length += sizeof(ChecksumEntry) + BLAKE3_OUT_LEN; checksum_header.entries++; ctx->header.featureCompatible |= AARU_FEATURE_RW_BLAKE3; } fseek(ctx->imageStream, checksum_position, SEEK_SET); TRACE("Writing checksum header"); fwrite(&checksum_header, sizeof(ChecksumHeader), 1, ctx->imageStream); // Add checksum block to index TRACE("Adding checksum block to index"); IndexEntry checksum_index_entry; checksum_index_entry.blockType = ChecksumBlock; checksum_index_entry.dataType = 0; checksum_index_entry.offset = checksum_position; utarray_push_back(ctx->indexEntries, &checksum_index_entry); TRACE("Added checksum block index entry at offset %" PRIu64, checksum_position); } /** * @brief Serialize the tracks metadata block and add it to the index. * * Writes a TracksHeader followed by the array of TrackEntry structures if any track entries are * present (tracksHeader.entries > 0 and trackEntries not NULL). The block is aligned to the DDT * block boundary and an IndexEntry is appended. * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @internal */ static void write_tracks_block(aaruformatContext *ctx) { // Write tracks block if(ctx->tracksHeader.entries <= 0 || ctx->trackEntries == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long tracks_position = ftell(ctx->imageStream); // Align index position to block boundary if needed uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(tracks_position & alignment_mask) { uint64_t aligned_position = tracks_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); tracks_position = aligned_position; } TRACE("Writing tracks block at position %ld", tracks_position); // Write header if(fwrite(&ctx->tracksHeader, sizeof(TracksHeader), 1, ctx->imageStream) == 1) { // Write entries size_t written_entries = fwrite(ctx->trackEntries, sizeof(TrackEntry), ctx->tracksHeader.entries, ctx->imageStream); if(written_entries == ctx->tracksHeader.entries) { TRACE("Successfully wrote tracks block with %u entries", ctx->tracksHeader.entries); // Add tracks block to index TRACE("Adding tracks block to index"); IndexEntry tracks_index_entry; tracks_index_entry.blockType = TracksBlock; tracks_index_entry.dataType = 0; tracks_index_entry.offset = tracks_position; utarray_push_back(ctx->indexEntries, &tracks_index_entry); TRACE("Added tracks block index entry at offset %" PRIu64, tracks_position); } } } /** * @brief Serialize a MODE 2 (XA) subheaders data block. * * When Compact Disc Mode 2 form sectors are present, optional 8-byte subheaders (one per logical * sector including negative / overflow ranges) are stored in an in-memory buffer. This function * writes that buffer as a DataBlock of type CompactDiscMode2Subheader with CRC64 (no compression) * and adds an IndexEntry. * * @param ctx Pointer to an initialized aaruformatContext in write mode; ctx->mode2_subheaders must * point to a buffer sized for the described sector span. * @internal */ static void write_mode2_subheaders_block(aaruformatContext *ctx) { // Write MODE 2 subheader data block if(ctx->mode2_subheaders == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long mode2_subheaders_position = ftell(ctx->imageStream); // Align index position to block boundary if needed uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(mode2_subheaders_position & alignment_mask) { uint64_t aligned_position = mode2_subheaders_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); mode2_subheaders_position = aligned_position; } TRACE("Writing MODE 2 subheaders block at position %ld", mode2_subheaders_position); BlockHeader subheaders_block = {0}; subheaders_block.identifier = DataBlock; subheaders_block.type = CompactDiscMode2Subheader; subheaders_block.compression = None; subheaders_block.length = (uint32_t)(ctx->userDataDdtHeader.negative + ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 8; subheaders_block.cmpLength = subheaders_block.length; // Calculate CRC64 subheaders_block.crc64 = aaruf_crc64_data(ctx->mode2_subheaders, subheaders_block.length); subheaders_block.cmpCrc64 = subheaders_block.crc64; // Write header if(fwrite(&subheaders_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data size_t written_bytes = fwrite(ctx->mode2_subheaders, subheaders_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote MODE 2 subheaders block (%" PRIu64 " bytes)", subheaders_block.length); // Add MODE 2 subheaders block to index TRACE("Adding MODE 2 subheaders block to index"); IndexEntry mode2_subheaders_index_entry; mode2_subheaders_index_entry.blockType = DataBlock; mode2_subheaders_index_entry.dataType = CompactDiscMode2Subheader; mode2_subheaders_index_entry.offset = mode2_subheaders_position; utarray_push_back(ctx->indexEntries, &mode2_subheaders_index_entry); TRACE("Added MODE 2 subheaders block index entry at offset %" PRIu64, mode2_subheaders_position); } } } /** * @brief Serialize the optional CD sector prefix block. * * The sector prefix corresponds to the leading bytes of a raw CD sector (synchronization pattern, * address / header in MSF format and the mode byte) that precede the user data and any ECC/ECCP * fields. It is unrelated to subchannel (P–W) data, which is handled separately. If prefix data * was collected (ctx->sector_prefix != NULL), this writes a DataBlock of type CdSectorPrefix * containing exactly the bytes accumulated up to sector_prefix_offset. The block is CRC64 * protected, uncompressed, aligned to the DDT block boundary and indexed. * * Typical raw Mode 1 / Mode 2 sector layout (2352 bytes total): * 12-byte sync pattern (00 FF FF FF FF FF FF FF FF FF FF 00) * 3-byte address (Minute, Second, Frame in BCD) * 1-byte mode (e.g., 0x01, 0x02) * ... user data ... * ... ECC / EDC ... * The stored prefix encompasses the first 16 bytes (sync + address + mode) or whatever subset * was gathered by the writer. * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @internal */ static void write_sector_prefix(aaruformatContext *ctx) { if(ctx->sector_prefix == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long prefix_position = ftell(ctx->imageStream); // Align index position to block boundary if needed uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(prefix_position & alignment_mask) { uint64_t aligned_position = prefix_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); prefix_position = aligned_position; } TRACE("Writing sector prefix block at position %ld", prefix_position); BlockHeader prefix_block = {0}; prefix_block.identifier = DataBlock; prefix_block.type = CdSectorPrefix; prefix_block.compression = None; prefix_block.length = (uint32_t)ctx->sector_prefix_offset; prefix_block.cmpLength = prefix_block.length; // Calculate CRC64 prefix_block.crc64 = aaruf_crc64_data(ctx->sector_prefix, prefix_block.length); prefix_block.cmpCrc64 = prefix_block.crc64; // Write header if(fwrite(&prefix_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data size_t written_bytes = fwrite(ctx->sector_prefix, prefix_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote CD sector prefix block (%" PRIu64 " bytes)", prefix_block.length); // Add prefix block to index TRACE("Adding CD sector prefix block to index"); IndexEntry prefix_index_entry; prefix_index_entry.blockType = DataBlock; prefix_index_entry.dataType = CdSectorPrefix; prefix_index_entry.offset = prefix_position; utarray_push_back(ctx->indexEntries, &prefix_index_entry); TRACE("Added CD sector prefix block index entry at offset %" PRIu64, prefix_position); } } } /** * @brief Serialize the optional CD sector suffix block (EDC/ECC region capture). * * The sector suffix contains trailing integrity and redundancy bytes of a raw CD sector that * follow the user data area. Depending on the mode this includes: * - Mode 1: 4-byte EDC, 8-byte reserved, 276 bytes (P/Q layers) ECC = 288 bytes total. * - Mode 2 Form 1: 4-byte EDC, 8 reserved, 276 ECC = 288 bytes. * - Mode 2 Form 2: 4-byte EDC only (no ECC) but when an error is detected the implementation * may still store a 288-byte suffix container for uniformity when capturing errored data. * * During writing, when an error or uncorrectable condition is detected for a sector's suffix, * the 288-byte trailing portion (starting at offset 2064 for Mode 1 / Form 1 or 2348/2349 for * other layouts) is copied into an in-memory expandable buffer pointed to by ctx->sector_suffix. * The per-sector DDT entry encodes either an inlined status (OK / No CRC / etc.) or an index to * the stored suffix (ctx->sector_suffix_offset / 288). This function serializes the accumulated * suffix buffer as a DataBlock of type CdSectorSuffix if any suffix bytes were stored. * * Layout considerations: * - The block length is exactly the number of bytes copied (ctx->sector_suffix_offset). * - No compression is applied; CRC64 is calculated on the raw suffix stream for integrity. * - The write position is aligned to the DDT block alignment (2^blockAlignmentShift). * * Indexing: An IndexEntry is appended so later readers can locate the suffix collection. Absence * of this block implies no per-sector suffix captures were required (all suffixes considered OK). * * Thread / reentrancy: This routine is called only once during finalization (aaruf_close) in a * single-threaded context; no synchronization is performed. * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * @internal */ static void write_sector_suffix(aaruformatContext *ctx) { if(ctx->sector_suffix == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long suffix_position = ftell(ctx->imageStream); // Align index position to block boundary if needed const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(suffix_position & alignment_mask) { const uint64_t aligned_position = suffix_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); suffix_position = aligned_position; } TRACE("Writing sector suffix block at position %ld", suffix_position); BlockHeader suffix_block = {0}; suffix_block.identifier = DataBlock; suffix_block.type = CdSectorSuffix; suffix_block.compression = None; suffix_block.length = (uint32_t)ctx->sector_suffix_offset; suffix_block.cmpLength = suffix_block.length; // Calculate CRC64 suffix_block.crc64 = aaruf_crc64_data(ctx->sector_suffix, suffix_block.length); suffix_block.cmpCrc64 = suffix_block.crc64; // Write header if(fwrite(&suffix_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_suffix, suffix_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote CD sector suffix block (%" PRIu64 " bytes)", suffix_block.length); // Add suffix block to index TRACE("Adding CD sector suffix block to index"); IndexEntry suffix_index_entry; suffix_index_entry.blockType = DataBlock; suffix_index_entry.dataType = CdSectorSuffix; suffix_index_entry.offset = suffix_position; utarray_push_back(ctx->indexEntries, &suffix_index_entry); TRACE("Added CD sector suffix block index entry at offset %" PRIu64, suffix_position); } } } /** * @brief Serialize the per-sector CD prefix status / index DeDuplication Table (DDT v2, prefix variant). * * This DDT records for each logical sector (including negative and overflow ranges) an optional * index into the stored 16‑byte prefix capture buffer plus a 4-bit status code. It is written only * if at least one prefix status or captured prefix was recorded (i.e., the in-memory DDT array exists). * * Encoding (current implementation uses the "mini" 16-bit form): * Bits 15..12 : SectorStatus enum value (see enums.h, values already positioned for v2 mini layout). * Bits 11..0 : 12-bit index (0..4095) of the 16-byte prefix chunk inside the CdSectorPrefix data block, * or 0 when no external prefix bytes were stored (status applies to a generated/implicit prefix). * * Notes: * - Unlike DDT v1, there are no CD_XFIX_MASK / CD_DFIX_MASK macros used here. The bit layout is compact * and directly encoded when writing (status values are pre-shifted where needed in write.c). * - Only the 16-bit mini variant is currently produced (sectorPrefixDdtMini). A 32-bit form is reserved * for future expansion (sectorPrefixDdt) but is not emitted unless populated. * - The table length equals (negative + Sectors + overflow) * entrySize. * - dataShift is set to 4 (2^4 = 16) expressing the granularity of referenced prefix units. * - No compression is applied; crc64/cmpCrc64 protect the raw table bytes. * - Idempotent: if an index entry of type DeDuplicationTable2 + CdSectorPrefixCorrected already exists * the function returns immediately. * * Alignment: The table is block-aligned using the same blockAlignmentShift as user data DDTs. * Indexing: An IndexEntry is appended on success so readers can locate and parse the table. * * @param ctx Pointer to a valid aaruformatContext in write mode (must not be NULL). * @internal */ static void write_sector_prefix_ddt(aaruformatContext *ctx) { if(ctx->sectorPrefixDdtMini == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long prefix_ddt_position = ftell(ctx->imageStream); // Align index position to block boundary if needed const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(prefix_ddt_position & alignment_mask) { const uint64_t aligned_position = prefix_ddt_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); prefix_ddt_position = aligned_position; } TRACE("Writing sector prefix DDT v2 at position %ld", prefix_ddt_position); DdtHeader2 ddt_header2 = {0}; ddt_header2.identifier = DeDuplicationTable2; ddt_header2.type = CdSectorPrefix; ddt_header2.compression = None; ddt_header2.levels = 1; ddt_header2.tableLevel = 0; ddt_header2.negative = ctx->userDataDdtHeader.negative; ddt_header2.overflow = ctx->userDataDdtHeader.overflow; ddt_header2.blockAlignmentShift = ctx->userDataDdtHeader.blockAlignmentShift; ddt_header2.dataShift = ctx->userDataDdtHeader.dataShift; ddt_header2.tableShift = 0; // Single-level DDT ddt_header2.sizeType = SmallDdtSizeType; ddt_header2.entries = ctx->imageInfo.Sectors + ctx->userDataDdtHeader.negative + ctx->userDataDdtHeader.overflow; ddt_header2.blocks = ctx->userDataDdtHeader.blocks; ddt_header2.start = 0; ddt_header2.length = ddt_header2.entries * sizeof(uint16_t); ddt_header2.cmpLength = ddt_header2.length; // Calculate CRC64 ddt_header2.crc64 = aaruf_crc64_data((uint8_t *)ctx->sectorPrefixDdtMini, (uint32_t)ddt_header2.length); ddt_header2.cmpCrc64 = ddt_header2.crc64; // Write header if(fwrite(&ddt_header2, sizeof(DdtHeader2), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sectorPrefixDdtMini, ddt_header2.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote sector prefix DDT v2 (%" PRIu64 " bytes)", ddt_header2.length); // Add prefix block to index TRACE("Adding sector prefix DDT v2 to index"); IndexEntry prefix_ddt_index_entry; prefix_ddt_index_entry.blockType = DeDuplicationTable2; prefix_ddt_index_entry.dataType = CdSectorPrefix; prefix_ddt_index_entry.offset = prefix_ddt_position; utarray_push_back(ctx->indexEntries, &prefix_ddt_index_entry); TRACE("Added sector prefix DDT v2 index entry at offset %" PRIu64, prefix_ddt_position); } } } /** * @brief Serialize the per-sector CD suffix status / index DeDuplication Table (DDT v2, suffix variant). * * This routine emits the DDT v2 table that maps each logical sector (including negative pregap * and overflow ranges) to (a) a 4-bit SectorStatus code and (b) a 12-bit index pointing into the * captured suffix data block (CdSectorSuffix). The suffix bytes (typically the 288-byte EDC/ECC * region for Mode 1 or Mode 2 Form 1, or shorter EDC-only for Form 2) are stored separately by * write_sector_suffix(). When a sector's suffix was captured because it differed from the expected * generated values (e.g., uncorrectable, intentionally preserved corruption, or variant layout), * the in-memory mini entry records the index of its 16 * 18 (288) byte chunk. If no suffix bytes * were explicitly stored for a sector the index field is zero and only the status applies. * * Encoding (mini 16-bit variant only, DDT v2 semantics): * Bits 15..12 : SectorStatus enumeration (already aligned for direct storage; no legacy masks used). * Bits 11..0 : 12-bit index (0..4095) referencing a suffix unit of size 288 bytes (2^dataShift granularity), * or 0 when the sector uses an implicit / regenerated suffix (no external data captured). * * Characteristics & constraints: * - Only DDT v2 is supported here; no fallback or mixed-mode emission with v1 occurs. * - Only the compact "mini" (16-bit) table form is currently produced (sectorSuffixDdtMini filled during write). * - Table length = (negative + total Sectors + overflow) * sizeof(uint16_t). * - dataShift mirrors userDataDdtHeader.dataShift (expressing granularity for index referencing). * - Single-level table (levels = 1, tableLevel = 0, tableShift = 0). * - CRC64 protects the raw uncompressed table (crc64 == cmpCrc64 because compression = None). * - Alignment: The table is aligned to 2^(blockAlignmentShift) before writing to guarantee block boundary access. * - Idempotence: If sectorSuffixDdtMini is NULL the function is a no-op (indicating no suffix anomalies captured). * * Index integration: * On success an IndexEntry (blockType = DeDuplicationTable2, dataType = CdSectorSuffix, offset = file position) * is appended to ctx->indexEntries enabling later readers to locate and parse the suffix DDT. * * Error handling & assumptions: * - The function does not explicitly propagate write failures upward; partial write errors simply * omit the index entry (TRACE logs provide diagnostics). Higher level close logic determines * overall success. * - Executed in a single-threaded finalization path; no locking is performed or required. * * Preconditions: * - ctx must be a valid non-NULL pointer opened for writing. * - ctx->sectorSuffixDdtMini must point to a fully populated contiguous array of uint16_t entries. * * @param ctx Active aaruformatContext being finalized. * @internal */ static void write_sector_suffix_ddt(aaruformatContext *ctx) { if(ctx->sectorSuffixDdtMini == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long suffix_ddt_position = ftell(ctx->imageStream); // Align index position to block boundary if needed const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(suffix_ddt_position & alignment_mask) { const uint64_t aligned_position = suffix_ddt_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); suffix_ddt_position = aligned_position; } TRACE("Writing sector suffix DDT v2 at position %ld", suffix_ddt_position); DdtHeader2 ddt_header2 = {0}; ddt_header2.identifier = DeDuplicationTable2; ddt_header2.type = CdSectorSuffix; ddt_header2.compression = None; ddt_header2.levels = 1; ddt_header2.tableLevel = 0; ddt_header2.negative = ctx->userDataDdtHeader.negative; ddt_header2.overflow = ctx->userDataDdtHeader.overflow; ddt_header2.blockAlignmentShift = ctx->userDataDdtHeader.blockAlignmentShift; ddt_header2.dataShift = ctx->userDataDdtHeader.dataShift; ddt_header2.tableShift = 0; // Single-level DDT ddt_header2.sizeType = SmallDdtSizeType; ddt_header2.entries = ctx->imageInfo.Sectors + ctx->userDataDdtHeader.negative + ctx->userDataDdtHeader.overflow; ddt_header2.blocks = ctx->userDataDdtHeader.blocks; ddt_header2.start = 0; ddt_header2.length = ddt_header2.entries * sizeof(uint16_t); ddt_header2.cmpLength = ddt_header2.length; // Calculate CRC64 ddt_header2.crc64 = aaruf_crc64_data((uint8_t *)ctx->sectorSuffixDdtMini, (uint32_t)ddt_header2.length); ddt_header2.cmpCrc64 = ddt_header2.crc64; // Write header if(fwrite(&ddt_header2, sizeof(DdtHeader2), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sectorSuffixDdtMini, ddt_header2.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote sector suffix DDT v2 (%" PRIu64 " bytes)", ddt_header2.length); // Add suffix block to index TRACE("Adding sector suffix DDT v2 to index"); IndexEntry suffix_ddt_index_entry; suffix_ddt_index_entry.blockType = DeDuplicationTable2; suffix_ddt_index_entry.dataType = CdSectorSuffix; suffix_ddt_index_entry.offset = suffix_ddt_position; utarray_push_back(ctx->indexEntries, &suffix_ddt_index_entry); TRACE("Added sector suffix DDT v2 index entry at offset %" PRIu64, suffix_ddt_position); } } } /** * @brief Serialize the per-sector subchannel or tag data block. * * This routine writes out the accumulated subchannel or tag metadata that accompanies each logical * sector (including negative pregap and overflow ranges). The exact interpretation and size depend * on the media type: * * **Optical Disc (CD) subchannel:** * - Type: CdSectorSubchannel * - Contains the deinterleaved P through W subchannel data (96 bytes per sector). * - Covers: (negative + Sectors + overflow) sectors. * - The P channel marks pause boundaries; Q encodes track/index/time information (MCN, ISRC). * - R–W channels are typically used for CD+G graphics or CD-TEXT. * * **Apple block media tags:** * - **AppleProfile / AppleFileWare:** 20 bytes per sector (AppleProfileTag). * - **AppleSonyDS / AppleSonySS:** 12 bytes per sector (AppleSonyTag). * - **PriamDataTower:** 24 bytes per sector (PriamDataTowerTag). * - Tags encode filesystem metadata, allocation state, or device-specific control information. * - Only positive sectors (0 through Sectors-1) and overflow are included; no negative range. * * The block size is computed as (applicable_sector_count) × (bytes_per_sector_for_media_type). * No compression is applied; the raw buffer is written verbatim after a DataBlock header with * CRC64 integrity protection. The write position is aligned to the DDT block boundary * (2^blockAlignmentShift) before serialization begins. * * **Media type validation:** * The function only proceeds if XmlMediaType is OpticalDisc or BlockMedia and (for block media) * the specific MediaType matches one of the supported Apple or Priam variants. Any other media * type causes an immediate silent return (logged at TRACE level). * * **Alignment & indexing:** * The block is aligned using the same alignment shift as the user data DDT. An IndexEntry * (blockType = DataBlock, dataType = subchannel_block.type, offset = aligned file position) is * appended to ctx->indexEntries on successful write, enabling readers to locate the subchannel * or tag data. * * **Thread / reentrancy:** * This function is invoked once during finalization (aaruf_close) in a single-threaded context. * No synchronization is performed. * * **Error handling:** * Write errors are logged but not explicitly propagated as return codes. If the write succeeds * an index entry is added; if it fails no entry is added and diagnostics appear in TRACE logs. * Higher level close logic determines overall success or failure. * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->sector_subchannel must point to a fully populated buffer sized appropriately * for the media type and sector count. * * @internal */ static void write_sector_subchannel(const aaruformatContext *ctx) { if(ctx->sector_subchannel == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); // Align index position to block boundary if needed const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing sector subchannel block at position %ld", block_position); BlockHeader subchannel_block = {0}; subchannel_block.identifier = DataBlock; subchannel_block.compression = None; if(ctx->imageInfo.XmlMediaType == OpticalDisc) { subchannel_block.type = CdSectorSubchannel; subchannel_block.length = (uint32_t)(ctx->userDataDdtHeader.negative + ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 96; } else if(ctx->imageInfo.XmlMediaType == BlockMedia) switch(ctx->imageInfo.MediaType) { case AppleProfile: case AppleFileWare: subchannel_block.type = AppleProfileTag; subchannel_block.length = (uint32_t)(ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 20; break; case AppleSonyDS: case AppleSonySS: subchannel_block.type = AppleSonyTag; subchannel_block.length = (uint32_t)(ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 12; break; case PriamDataTower: subchannel_block.type = PriamDataTowerTag; subchannel_block.length = (uint32_t)(ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 24; break; default: TRACE("Incorrect media type, not writing sector subchannel block"); return; // Incorrect media type } else { TRACE("Incorrect media type, not writing sector subchannel block"); return; // Incorrect media type } subchannel_block.cmpLength = subchannel_block.length; // Calculate CRC64 subchannel_block.crc64 = aaruf_crc64_data(ctx->sector_subchannel, subchannel_block.length); subchannel_block.cmpCrc64 = subchannel_block.crc64; // Write header if(fwrite(&subchannel_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_subchannel, subchannel_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote sector subchannel block (%" PRIu64 " bytes)", subchannel_block.length); // Add subchannel block to index TRACE("Adding sector subchannel block to index"); IndexEntry subchannel_index_entry; subchannel_index_entry.blockType = DataBlock; subchannel_index_entry.dataType = subchannel_block.type; subchannel_index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &subchannel_index_entry); TRACE("Added sector subchannel block index entry at offset %" PRIu64, block_position); } } } /** * @brief Serialize DVD long sector auxiliary data blocks to the image file. * * This function writes four separate data blocks containing DVD-specific auxiliary information * extracted from "long" DVD sectors. DVD long sectors contain additional fields beyond the 2048 * bytes of user data, including sector identification, error detection, and copy protection * information. When writing DVD images with long sector support, these auxiliary fields are * stored separately from the main user data to optimize storage and enable selective access. * * The function is only invoked if all four auxiliary buffers have been populated during image * creation (sector_id, sector_ied, sector_cpr_mai, sector_edc). If any buffer is NULL, the * function returns immediately without writing anything, allowing DVD images without long sector * data to be created normally. * * **Four auxiliary data blocks written:** * * 1. **DVD Sector ID Block (DvdSectorId)**: 4 bytes per sector * - Contains the sector ID field from DVD long sectors * - Used for sector identification and addressing validation * * 2. **DVD Sector IED Block (DvdSectorIed)**: 2 bytes per sector * - Contains the IED (ID Error Detection) field from DVD long sectors * - Used for detecting errors in the sector ID field * * 3. **DVD Sector CPR/MAI Block (DvdSectorCprMai)**: 6 bytes per sector * - Contains the CPR_MAI (Copyright Management Information) field * - Used for copy protection and media authentication information * * 4. **DVD Sector EDC Block (DvdSectorEdc)**: 4 bytes per sector * - Contains the EDC (Error Detection Code) field from DVD long sectors * - Used for detecting errors in the sector data * * **Block structure for each auxiliary block:** * Each block consists of: * 1. BlockHeader containing identifier (DataBlock), type (DvdSectorId/IED/CprMai/Edc), * compression (None), lengths, and CRC64 checksums * 2. Raw auxiliary data: concatenated fields from all sectors (including negative, normal, * and overflow sectors) * * The total number of sectors includes negative sectors (for lead-in), normal image sectors, * and overflow sectors (for lead-out), calculated as: * total_sectors = negative + Sectors + overflow * * **Write sequence for each block:** * 1. Seek to end of file * 2. Align file position to block boundary (using blockAlignmentShift) * 3. Construct BlockHeader with appropriate type and calculated length * 4. Calculate CRC64 over the auxiliary data buffer * 5. Write BlockHeader (sizeof(BlockHeader) bytes) * 6. Write auxiliary data buffer (length bytes) * 7. On success, add IndexEntry to ctx->indexEntries * * **Alignment and file positioning:** * Before writing each block, the file position is moved to EOF and then aligned forward to the * next boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived * from ctx->userDataDdtHeader.blockAlignmentShift. This ensures all blocks begin on properly * aligned offsets for efficient I/O and compliance with the Aaru format specification. * * **Index registration:** * After successfully writing each block's header and data, an IndexEntry is appended to * ctx->indexEntries with: * - blockType = DataBlock * - dataType = DvdSectorId, DvdSectorIed, DvdSectorCprMai, or DvdSectorEdc * - offset = the aligned file position where the BlockHeader was written * * **Error handling:** * Write errors (fwrite returning < 1) are silently ignored for individual blocks; no index entry * is added if a write fails, but iteration continues to attempt writing remaining blocks. * Diagnostic TRACE logs report success or failure for each block. The function does not propagate * error codes; higher-level close logic must validate overall integrity if needed. * * **No-op conditions:** * - ctx->sector_id is NULL OR * - ctx->sector_ied is NULL OR * - ctx->sector_cpr_mai is NULL OR * - ctx->sector_edc is NULL * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->sector_id contains the ID fields from all DVD long sectors (may be NULL). * ctx->sector_ied contains the IED fields from all DVD long sectors (may be NULL). * ctx->sector_cpr_mai contains the CPR/MAI fields from all DVD long sectors (may be NULL). * ctx->sector_edc contains the EDC fields from all DVD long sectors (may be NULL). * ctx->imageStream must be open and writable. ctx->indexEntries must be initialized * (utarray) to accept new index entries. * * @note DVD Long Sector Format: * - Standard DVD sectors contain 2048 bytes of user data * - Long DVD sectors include additional fields for error detection and copy protection * - Total long sector size varies by DVD format (typically 2064-2076 bytes) * - These auxiliary fields are critical for forensic imaging and copy-protected media * * @note Sector Coverage: * - Negative sectors: Lead-in area before sector 0 (if present) * - Normal sectors: Main data area (sectors 0 to Sectors-1) * - Overflow sectors: Lead-out area after the main data (if present) * - All three areas are included in the auxiliary data blocks * * @note Field Sizes: * - ID field: 4 bytes per sector (sector identification) * - IED field: 2 bytes per sector (ID error detection) * - CPR/MAI field: 6 bytes per sector (copyright management) * - EDC field: 4 bytes per sector (error detection code) * - Total auxiliary data: 16 bytes per sector across all four blocks * * @note Memory Management: * - The function does not allocate or free any memory * - Auxiliary data buffers are managed by the caller * - Buffers are written directly without modification * - Memory is freed later during context cleanup (aaruf_close) * * @note Use Cases: * - Forensic imaging of DVD media requiring complete sector data * - Preservation of copy-protected DVD content * - Analysis of DVD error detection and correction information * - Validation of DVD sector structure and integrity * - Research into DVD copy protection mechanisms * * @note Order in Close Sequence: * - DVD long sector blocks are typically written after user data but before metadata * - The exact position in the file depends on what other blocks precede them * - Index entries ensure blocks can be located during subsequent opens * - All four blocks are written consecutively if present * * @warning The auxiliary data buffers must contain data for ALL sectors (negative + normal + * overflow). Partial buffers or mismatched sizes will cause incorrect data to be * written or buffer overruns. * * @warning No compression is applied to DVD auxiliary data blocks. They are stored as raw * binary data, which may result in larger image files for DVD media. * * @warning If any of the four auxiliary buffers is NULL, the entire function is skipped. * This is an all-or-nothing operation - either all four blocks are written or none. * * @see aaruf_write_sector_long() for writing individual DVD long sectors that populate these buffers. * @see BlockHeader for the block header structure definition. * * @internal */ void write_dvd_long_sector_blocks(aaruformatContext *ctx) { if(ctx->sector_id == NULL || ctx->sector_ied == NULL || ctx->sector_cpr_mai == NULL || ctx->sector_edc == NULL) return; uint64_t total_sectors = ctx->userDataDdtHeader.negative + ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow; // Write DVD sector ID block fseek(ctx->imageStream, 0, SEEK_END); long id_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(id_position & alignment_mask) { const uint64_t aligned_position = id_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); id_position = aligned_position; } TRACE("Writing DVD sector ID block at position %ld", id_position); BlockHeader id_block = {0}; id_block.identifier = DataBlock; id_block.type = DvdSectorId; id_block.compression = None; id_block.length = (uint32_t)total_sectors * 4; id_block.cmpLength = id_block.length; // Calculate CRC64 id_block.crc64 = aaruf_crc64_data(ctx->sector_id, id_block.length); id_block.cmpCrc64 = id_block.crc64; // Write header if(fwrite(&id_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_id, id_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote DVD sector ID block (%" PRIu64 " bytes)", id_block.length); // Add ID block to index TRACE("Adding DVD sector ID block to index"); IndexEntry id_index_entry; id_index_entry.blockType = DataBlock; id_index_entry.dataType = DvdSectorId; id_index_entry.offset = id_position; utarray_push_back(ctx->indexEntries, &id_index_entry); TRACE("Added DVD sector ID block index entry at offset %" PRIu64, id_position); } } // Write DVD sector IED block fseek(ctx->imageStream, 0, SEEK_END); long ied_position = ftell(ctx->imageStream); if(ied_position & alignment_mask) { const uint64_t aligned_position = ied_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); ied_position = aligned_position; } TRACE("Writing DVD sector IED block at position %ld", ied_position); BlockHeader ied_block = {0}; ied_block.identifier = DataBlock; ied_block.type = DvdSectorIed; ied_block.compression = None; ied_block.length = (uint32_t)total_sectors * 2; ied_block.cmpLength = ied_block.length; // Calculate CRC64 ied_block.crc64 = aaruf_crc64_data(ctx->sector_ied, ied_block.length); ied_block.cmpCrc64 = ied_block.crc64; // Write header if(fwrite(&ied_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_ied, ied_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote DVD sector IED block (%" PRIu64 " bytes)", ied_block.length); // Add IED block to index TRACE("Adding DVD sector IED block to index"); IndexEntry ied_index_entry; ied_index_entry.blockType = DataBlock; ied_index_entry.dataType = DvdSectorIed; ied_index_entry.offset = ied_position; utarray_push_back(ctx->indexEntries, &ied_index_entry); TRACE("Added DVD sector IED block index entry at offset %" PRIu64, ied_position); } } // Write DVD sector CPR/MAI block fseek(ctx->imageStream, 0, SEEK_END); long cpr_mai_position = ftell(ctx->imageStream); if(cpr_mai_position & alignment_mask) { const uint64_t aligned_position = cpr_mai_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); cpr_mai_position = aligned_position; } TRACE("Writing DVD sector CPR/MAI block at position %ld", cpr_mai_position); BlockHeader cpr_mai_block = {0}; cpr_mai_block.identifier = DataBlock; cpr_mai_block.type = DvdSectorCprMai; cpr_mai_block.compression = None; cpr_mai_block.length = (uint32_t)total_sectors * 6; cpr_mai_block.cmpLength = cpr_mai_block.length; // Calculate CRC64 cpr_mai_block.crc64 = aaruf_crc64_data(ctx->sector_cpr_mai, cpr_mai_block.length); cpr_mai_block.cmpCrc64 = cpr_mai_block.crc64; // Write header if(fwrite(&cpr_mai_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_cpr_mai, cpr_mai_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote DVD sector CPR/MAI block (%" PRIu64 " bytes)", cpr_mai_block.length); // Add CPR/MAI block to index TRACE("Adding DVD sector CPR/MAI block to index"); IndexEntry cpr_mai_index_entry; cpr_mai_index_entry.blockType = DataBlock; cpr_mai_index_entry.dataType = DvdSectorCprMai; cpr_mai_index_entry.offset = cpr_mai_position; utarray_push_back(ctx->indexEntries, &cpr_mai_index_entry); TRACE("Added DVD sector CPR/MAI block index entry at offset %" PRIu64, cpr_mai_position); } } // Write DVD sector EDC block fseek(ctx->imageStream, 0, SEEK_END); long edc_position = ftell(ctx->imageStream); if(edc_position & alignment_mask) { const uint64_t aligned_position = edc_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); edc_position = aligned_position; } TRACE("Writing DVD sector EDC block at position %ld", edc_position); BlockHeader edc_block = {0}; edc_block.identifier = DataBlock; edc_block.type = DvdSectorEdc; edc_block.compression = None; edc_block.length = (uint32_t)total_sectors * 4; edc_block.cmpLength = edc_block.length; // Calculate CRC64 edc_block.crc64 = aaruf_crc64_data(ctx->sector_edc, edc_block.length); edc_block.cmpCrc64 = edc_block.crc64; // Write header if(fwrite(&edc_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_edc, edc_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote DVD sector EDC block (%" PRIu64 " bytes)", edc_block.length); // Add EDC block to index TRACE("Adding DVD sector EDC block to index"); IndexEntry edc_index_entry; edc_index_entry.blockType = DataBlock; edc_index_entry.dataType = DvdSectorEdc; edc_index_entry.offset = edc_position; utarray_push_back(ctx->indexEntries, &edc_index_entry); TRACE("Added DVD sector EDC block index entry at offset %" PRIu64, edc_position); } } } /** * @brief Serialize the DVD decrypted title key data block to the image file. * * This function writes a data block containing decrypted DVD title keys for all sectors * in the image. DVD title keys are used in the Content Scrambling System (CSS) encryption * scheme to decrypt sector data on encrypted DVDs. When imaging encrypted DVD media, if * the decryption keys are available, they can be stored alongside the encrypted data to * enable future decryption without requiring the original disc or authentication process. * * The function is only invoked if the decrypted title key buffer has been populated during * image creation (ctx->sector_decrypted_title_key != NULL). If the buffer is NULL, the * function returns immediately without writing anything, allowing DVD images without * decrypted title keys to be created normally. This is typical for non-encrypted DVDs * or when keys were not available during imaging. * * **Data block structure:** * * - **Block Type**: DataBlock with type DvdSectorTitleKeyDecrypted * - **Size**: 5 bytes per sector (total_sectors × 5 bytes) * - total_sectors = negative sectors + user sectors + overflow sectors * - **Compression**: None (stored uncompressed) * - **CRC64**: Computed over the entire decrypted title key buffer * - **Alignment**: Block-aligned according to ctx->userDataDdtHeader.blockAlignmentShift * * **Block write sequence:** * * 1. Check if ctx->sector_decrypted_title_key is NULL; return early if so * 2. Seek to end of file to determine write position * 3. Align file position forward to next block boundary (if needed) * 4. Construct BlockHeader with: * - identifier = DataBlock * - type = DvdSectorTitleKeyDecrypted * - compression = None * - length = cmpLength = (negative + sectors + overflow) × 5 * - crc64 = cmpCrc64 = CRC64 of entire key buffer * 5. Write BlockHeader (sizeof(BlockHeader) bytes) * 6. Write decrypted title key data buffer * 7. Create and append IndexEntry to ctx->indexEntries: * - blockType = DataBlock * - dataType = DvdSectorTitleKeyDecrypted * - offset = aligned block position * * **Alignment and file positioning:** * * Before writing the block, the file position is moved to EOF and then aligned forward * to the next boundary satisfying (position & alignment_mask) == 0, where alignment_mask * is derived from the blockAlignmentShift. This ensures that all structural blocks begin * on properly aligned offsets for efficient I/O and compliance with the Aaru format * specification. * * **Index registration:** * * After successful write, an IndexEntry is created and added to ctx->indexEntries using * utarray_push_back(). This allows the block to be located efficiently during image * reading via the index lookup mechanism. The index stores the exact file offset where * the BlockHeader was written. * * **Title Key Format:** * * Each sector's title key is exactly 5 bytes. The keys are stored sequentially in sector * order (negative sectors first, then user sectors, then overflow sectors). The corrected * sector addressing scheme is used to index into the buffer: * - Negative sector N: index = (N - negative) * - User sector U: index = (negative + U) * - Overflow sector O: index = (negative + Sectors + O) * * @param ctx Pointer to an initialized aaruformatContext in write mode. * * @note This is a static helper function called during image finalization (aaruf_close). * It is not part of the public API. * * @note The function performs no error handling beyond checking for NULL buffer. * Write failures are silently ignored, consistent with other optional metadata * serialization routines in the finalization sequence. * * @note Decrypted title keys are sensitive cryptographic material. Applications should * consider access control and security implications when storing and distributing * images containing decrypted keys. * * @note The function uses TRACE() macros for diagnostic logging of write progress, * file positions, and index entry creation. * * @warning This function assumes ctx->imageStream is open and writable. Calling it * with a read-only or closed stream will result in undefined behavior. * * @warning The function does not validate that ctx->sector_decrypted_title_key contains * exactly (negative + sectors + overflow) × 5 bytes. Buffer overruns may occur * if the buffer was improperly allocated. * * @warning Do not call this function directly. It is invoked automatically by aaruf_close() * as part of the image finalization sequence. * * @internal */ static void write_dvd_title_key_decrypted_block(const aaruformatContext *ctx) { if(ctx->sector_decrypted_title_key == NULL) return; fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing DVD decrypted title key block at position %ld", block_position); BlockHeader decrypted_title_key_block = {0}; decrypted_title_key_block.identifier = DataBlock; decrypted_title_key_block.type = DvdSectorTitleKeyDecrypted; decrypted_title_key_block.compression = None; decrypted_title_key_block.length = (uint32_t)(ctx->userDataDdtHeader.negative + ctx->imageInfo.Sectors + ctx->userDataDdtHeader.overflow) * 5; decrypted_title_key_block.cmpLength = decrypted_title_key_block.length; // Calculate CRC64 decrypted_title_key_block.crc64 = aaruf_crc64_data(ctx->sector_decrypted_title_key, decrypted_title_key_block.length); decrypted_title_key_block.cmpCrc64 = decrypted_title_key_block.crc64; // Write header if(fwrite(&decrypted_title_key_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(ctx->sector_decrypted_title_key, decrypted_title_key_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote DVD decrypted title key block (%" PRIu64 " bytes)", decrypted_title_key_block.length); // Add decrypted title key block to index TRACE("Adding DVD decrypted title key block to index"); IndexEntry decrypted_title_key_index_entry; decrypted_title_key_index_entry.blockType = DataBlock; decrypted_title_key_index_entry.dataType = DvdSectorTitleKeyDecrypted; decrypted_title_key_index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &decrypted_title_key_index_entry); TRACE("Added DVD decrypted title key block index entry at offset %" PRIu64, block_position); } } } /** * @brief Serialize all accumulated media tags to the image file. * * Media tags represent arbitrary metadata or descriptor blobs associated with the entire medium * (as opposed to per-sector or per-track metadata). Examples include proprietary drive firmware * information, TOC descriptors, ATIP data, PMA/Lead-in content, or manufacturer-specific binary * structures that do not fit the standard track or sector model. Each tag is identified by a * numeric type field interpreted by upper layers or external tooling. * * This function traverses the ctx->mediaTags hash table (keyed by tag type) using HASH_ITER and * writes each tag as an independent DataBlock. Each block is: * - Aligned to the DDT block boundary (controlled by ctx->userDataDdtHeader.blockAlignmentShift) * - Prefixed with a BlockHeader containing the identifier DataBlock and a data type derived * from the tag's type field via ::aaruf_get_datatype_for_media_tag_type() * - Uncompressed (compression = None); both length and cmpLength are set to the tag's byte count * - CRC64-protected: the checksum is computed over the raw tag data and stored in both crc64 * and cmpCrc64 fields of the BlockHeader * - Followed immediately by the tag's data payload * * After successfully writing a tag's header and data, an IndexEntry is appended to * ctx->indexEntries with: * - blockType = DataBlock * - dataType = the converted tag type (from aaruf_get_datatype_for_media_tag_type) * - offset = the aligned file position where the BlockHeader was written * * **Alignment and file positioning:** * Before writing each tag, the file position is moved to EOF and then aligned forward to the next * boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived from the * blockAlignmentShift. This ensures that all structural blocks (including media tags) begin on * properly aligned offsets for efficient I/O and compliance with the Aaru format specification. * * **Order of operations for each tag:** * 1. Seek to end of file * 2. Align file position to block boundary * 3. Construct BlockHeader with identifier, type, length, CRC64 * 4. Write BlockHeader (sizeof(BlockHeader) bytes) * 5. Write tag data (tag->length bytes) * 6. On success, push IndexEntry to ctx->indexEntries * * **Error handling:** * Write errors (fwrite returning < 1) are silently ignored for individual tags; no index entry is * added if a write fails, but iteration continues. Diagnostic TRACE logs report success or * failure for each tag. The function does not propagate error codes; higher-level close logic * must validate overall integrity if needed. * * **Hash table iteration:** * The function uses HASH_ITER(hh, ctx->mediaTags, media_tag, tmp_media_tag) from uthash to * safely iterate all entries. The tmp_media_tag parameter provides deletion-safe traversal, * though this function does not delete entries (cleanup is handled during context teardown). * * **No-op conditions:** * If ctx->mediaTags is NULL (no tags were added during image creation), the function returns * immediately without writing anything or modifying the index. * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->mediaTags contains the hash table of media tags to serialize (may be NULL * if no tags exist). ctx->imageStream must be open and writable. ctx->indexEntries * must be initialized (utarray) to accept new index entries. * * @note Media tags are format-agnostic at this layer. The tag type-to-datatype mapping is * delegated to ::aaruf_get_datatype_for_media_tag_type(), which consults internal * tables or enumerations defined elsewhere in the library. * * @see ::aaruf_write_media_tag() for adding tags to the context during image creation. * @see ::aaruf_get_datatype_for_media_tag_type() for type conversion logic. * @see mediaTagEntry for the hash table entry structure. * * @internal */ static void write_media_tags(const aaruformatContext *ctx) { if(ctx->mediaTags == NULL) return; mediaTagEntry *media_tag = NULL; mediaTagEntry *tmp_media_tag = NULL; HASH_ITER(hh, ctx->mediaTags, media_tag, tmp_media_tag) { fseek(ctx->imageStream, 0, SEEK_END); long tag_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(tag_position & alignment_mask) { const uint64_t aligned_position = tag_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); tag_position = aligned_position; } TRACE("Writing media tag block type %d at position %ld", aaruf_get_datatype_for_media_tag_type(media_tag->type), tag_position); BlockHeader tag_block = {0}; tag_block.identifier = DataBlock; tag_block.type = (uint16_t)aaruf_get_datatype_for_media_tag_type(media_tag->type); tag_block.compression = None; tag_block.length = media_tag->length; tag_block.cmpLength = tag_block.length; // Calculate CRC64 tag_block.crc64 = aaruf_crc64_data(media_tag->data, tag_block.length); tag_block.cmpCrc64 = tag_block.crc64; // Write header if(fwrite(&tag_block, sizeof(BlockHeader), 1, ctx->imageStream) == 1) { // Write data const size_t written_bytes = fwrite(media_tag->data, tag_block.length, 1, ctx->imageStream); if(written_bytes == 1) { TRACE("Successfully wrote media tag block type %d (%" PRIu64 " bytes)", tag_block.type, tag_block.length); // Add media tag block to index TRACE("Adding media tag type %d block to index", tag_block.type); IndexEntry tag_index_entry; tag_index_entry.blockType = DataBlock; tag_index_entry.dataType = tag_block.type; tag_index_entry.offset = tag_position; utarray_push_back(ctx->indexEntries, &tag_index_entry); TRACE("Added media tag block type %d index entry at offset %" PRIu64, tag_block.type, tag_position); } } } } /** * @brief Serialize the geometry metadata block to the image file. * * This function writes a GeometryBlockHeader containing legacy CHS (Cylinder-Head-Sector) style * logical geometry metadata to the Aaru image file. The geometry block records the physical/logical * layout of media that can be addressed using classical CHS parameters (cylinders, heads, sectors * per track) common to legacy hard disk drives and some optical media formats. * * The geometry information is optional; if no geometry metadata was previously set (detected by * checking ctx->geometryBlock.identifier != GeometryBlock), the function returns immediately as a * no-op. When present, the block is written at the end of the image file, aligned to the DDT block * boundary specified by blockAlignmentShift, and an IndexEntry is appended to ctx->indexEntries so * readers can locate it during image parsing. * * Block layout: * - GeometryBlockHeader (16 bytes): * - identifier (4 bytes): BlockType::GeometryBlock magic constant * - cylinders (4 bytes): Number of cylinders * - heads (4 bytes): Number of heads (tracks per cylinder) * - sectorsPerTrack (4 bytes): Number of sectors per track * - No additional payload follows the header in current format versions. * * Total logical sectors implied by the geometry: cylinders × heads × sectorsPerTrack. * Sector size is not encoded in this block and must be derived from other metadata (e.g., from * the media type or explicitly stored elsewhere in the image). * * Alignment strategy: * - The write position is obtained via fseek(SEEK_END) + ftell(). * - If the position is not aligned to (1 << blockAlignmentShift), it is advanced to the next * aligned boundary by computing: (position + alignment_mask) & ~alignment_mask. * - This ensures the geometry block starts on a block-aligned offset for efficient access. * * Error handling: * - If fwrite() fails to write the GeometryBlockHeader, the function silently returns without * updating the index. This is consistent with other optional metadata writers in this module * that use opportunistic writes (failures logged via TRACE but not propagated as errors). * - The caller (aaruf_close) will continue finalizing other blocks even if geometry write fails. * * Indexing: * - On successful write, an IndexEntry with blockType = GeometryBlock, dataType = 0, and * offset = block_position is pushed to ctx->indexEntries. * - The index will be serialized later by write_index_block() and allows readers to quickly * locate the geometry metadata without scanning the entire file. * * Use cases: * - Preserving original CHS geometry for disk images from legacy systems (e.g., IDE/PATA drives, * floppy disks, early SCSI devices) where BIOS or firmware relied on CHS addressing. * - Documenting physical layout of optical media that may have track/sector organization. * - Supporting forensic/archival workflows that need complete metadata fidelity. * * Thread safety: This function is not thread-safe; it modifies shared ctx state (imageStream file * position, indexEntries array) and must only be called during single-threaded finalization * (within aaruf_close). * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * The geometryBlock field must be pre-populated if geometry metadata is desired. * The imageStream must be open and writable. * @internal * @see GeometryBlockHeader * @see aaruf_set_geometry() for setting geometry values before closing. */ static void write_geometry_block(const aaruformatContext *ctx) { if(ctx->geometryBlock.identifier != GeometryBlock) return; fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing geometry block at position %ld", block_position); // Write header if(fwrite(&ctx->geometryBlock, sizeof(GeometryBlockHeader), 1, ctx->imageStream) == 1) { TRACE("Successfully wrote geometry block"); // Add geometry block to index TRACE("Adding geometry block to index"); IndexEntry index_entry; index_entry.blockType = GeometryBlock; index_entry.dataType = 0; index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &index_entry); TRACE("Added geometry block index entry at offset %" PRIu64, block_position); } } /** * @brief Serialize the metadata block containing image and media descriptive information. * * This function writes a MetadataBlock containing human-readable and machine-readable metadata * strings that describe the image creation context, the physical media being preserved, and the * drive used for acquisition. The metadata block stores variable-length UTF-16LE strings for fields * such as creator identification, user comments, media identification (title, manufacturer, model, * serial number, barcode, part number), and drive identification (manufacturer, model, serial * number, firmware revision). Each string is stored sequentially in a single contiguous buffer, * with the MetadataBlockHeader recording both the offset (relative to the start of the buffer) * and length of each field. * * The metadata block is optional; if no metadata fields have been populated (all string pointers * are NULL and sequence numbers are zero), the function returns immediately without writing * anything. This no-op behavior is detected by checking that the identifier has not been * explicitly set to MetadataBlock and all relevant imageInfo string fields are NULL. * * **Block structure:** * The serialized block consists of: * 1. MetadataBlockHeader (fixed size, containing identifier, sequence numbers, field offsets * and lengths for all metadata strings) * 2. Variable-length payload: concatenated UTF-16LE string data for all non-NULL fields * * The total blockSize is computed as sizeof(MetadataBlockHeader) plus the sum of all populated * string lengths (creatorLength, commentsLength, mediaTitleLength, etc.). Note that lengths are * in bytes, not character counts. * * **Field packing order:** * Non-NULL strings from ctx->imageInfo are copied into the buffer in the following order: * 1. Creator * 2. Comments * 3. MediaTitle * 4. MediaManufacturer * 5. MediaModel * 6. MediaSerialNumber * 7. MediaBarcode * 8. MediaPartNumber * 9. DriveManufacturer * 10. DriveModel * 11. DriveSerialNumber * 12. DriveFirmwareRevision * * As each field is copied, its offset (relative to the buffer start, which begins after the * header) is recorded in the corresponding offset field of ctx->metadataBlockHeader, and the * position pointer is advanced by the field's length. * * **Alignment and file positioning:** * Before writing the block, the file position is moved to EOF and then aligned forward to the * next boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived * from ctx->userDataDdtHeader.blockAlignmentShift. This ensures the metadata block begins on * a properly aligned offset for efficient I/O and compliance with the Aaru format specification. * * **Index registration:** * After successfully writing the metadata block, an IndexEntry is appended to ctx->indexEntries * with: * - blockType = MetadataBlock * - dataType = 0 (metadata blocks have no subtype) * - offset = the aligned file position where the block was written * * **Memory management:** * The function allocates a temporary buffer (via calloc) sized to hold the entire block payload. * If allocation fails, the function returns immediately without writing anything. The buffer is * freed before the function returns, regardless of write success or failure. * * **Error handling:** * Write errors (fwrite returning < 1) are silently ignored; no index entry is added if the write * fails, but the temporary buffer is still freed. Diagnostic TRACE logs report success or failure. * The function does not propagate error codes; higher-level close logic must validate overall * integrity if needed. * * **No-op conditions:** * - ctx->metadataBlockHeader.identifier is not MetadataBlock AND * - ctx->metadataBlockHeader.mediaSequence == 0 AND * - ctx->metadataBlockHeader.lastMediaSequence == 0 AND * - All ctx->imageInfo string fields (Creator, Comments, MediaTitle, etc.) are NULL * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->metadataBlockHeader contains the header template with pre-populated field * lengths and sequence numbers (if applicable). ctx->imageInfo contains pointers * to the actual UTF-16LE string data (may be NULL for unpopulated fields). * ctx->imageStream must be open and writable. ctx->indexEntries must be initialized * (utarray) to accept new index entries. * * @note UTF-16LE Encoding: * - All metadata strings use UTF-16LE encoding to support international characters * - Field lengths are in bytes, not character counts (UTF-16LE uses 2 or 4 bytes per character) * - The library treats string data as opaque and does not validate UTF-16LE encoding * - Ensure even byte lengths to maintain UTF-16LE character alignment * * @note Unlike data blocks (which include CRC64 checksums), the metadata block does not currently * include integrity checking beyond the implicit file-level checksums. The header itself * stores offsets/lengths but not CRCs for individual string fields. * * @note Media sequence numbers (mediaSequence, lastMediaSequence) support multi-volume image * sets (e.g., spanning multiple optical discs). Single-volume images typically set both * to 0 or leave them uninitialized. * * @see MetadataBlockHeader for the on-disk structure definition. * @see ::aaruf_set_creator() for populating the creator field. * @see ::aaruf_set_comments() for populating the comments field. * @see ::aaruf_set_media_title() for populating the media title field. * * @internal */ static void write_metadata_block(aaruformatContext *ctx) { if(ctx->metadataBlockHeader.identifier != MetadataBlock && ctx->metadataBlockHeader.mediaSequence == 0 && ctx->metadataBlockHeader.lastMediaSequence == 0 && ctx->imageInfo.Creator == NULL && ctx->imageInfo.Comments == NULL && ctx->imageInfo.MediaTitle == NULL && ctx->imageInfo.MediaManufacturer == NULL && ctx->imageInfo.MediaModel == NULL && ctx->imageInfo.MediaSerialNumber == NULL && ctx->imageInfo.MediaBarcode == NULL && ctx->imageInfo.MediaPartNumber == NULL && ctx->imageInfo.DriveManufacturer == NULL && ctx->imageInfo.DriveModel == NULL && ctx->imageInfo.DriveSerialNumber == NULL && ctx->imageInfo.DriveFirmwareRevision == NULL) return; ctx->metadataBlockHeader.blockSize = sizeof(MetadataBlockHeader) + ctx->metadataBlockHeader.creatorLength + ctx->metadataBlockHeader.commentsLength + ctx->metadataBlockHeader.mediaTitleLength + ctx->metadataBlockHeader.mediaManufacturerLength + ctx->metadataBlockHeader.mediaModelLength + ctx->metadataBlockHeader.mediaSerialNumberLength + ctx->metadataBlockHeader.mediaBarcodeLength + ctx->metadataBlockHeader.mediaPartNumberLength + ctx->metadataBlockHeader.driveManufacturerLength + ctx->metadataBlockHeader.driveModelLength + ctx->metadataBlockHeader.driveSerialNumberLength + ctx->metadataBlockHeader.driveFirmwareRevisionLength; ctx->metadataBlockHeader.identifier = MetadataBlock; int pos = sizeof(MetadataBlockHeader); uint8_t *buffer = calloc(1, ctx->metadataBlockHeader.blockSize); if(buffer == NULL) return; if(ctx->imageInfo.Creator != NULL && ctx->metadataBlockHeader.creatorLength > 0) { memcpy(buffer + pos, ctx->imageInfo.Creator, ctx->metadataBlockHeader.creatorLength); ctx->metadataBlockHeader.creatorOffset = pos; pos += ctx->metadataBlockHeader.creatorLength; } if(ctx->imageInfo.Comments != NULL && ctx->metadataBlockHeader.commentsLength > 0) { memcpy(buffer + pos, ctx->imageInfo.Comments, ctx->metadataBlockHeader.commentsLength); ctx->metadataBlockHeader.commentsOffset = pos; pos += ctx->metadataBlockHeader.commentsLength; } if(ctx->imageInfo.MediaTitle != NULL && ctx->metadataBlockHeader.mediaTitleLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaTitle, ctx->metadataBlockHeader.mediaTitleLength); ctx->metadataBlockHeader.mediaTitleOffset = pos; pos += ctx->metadataBlockHeader.mediaTitleLength; } if(ctx->imageInfo.MediaManufacturer != NULL && ctx->metadataBlockHeader.mediaManufacturerLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaManufacturer, ctx->metadataBlockHeader.mediaManufacturerLength); ctx->metadataBlockHeader.mediaManufacturerOffset = pos; pos += ctx->metadataBlockHeader.mediaManufacturerLength; } if(ctx->imageInfo.MediaModel != NULL && ctx->metadataBlockHeader.mediaModelLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaModel, ctx->metadataBlockHeader.mediaModelLength); ctx->metadataBlockHeader.mediaModelOffset = pos; pos += ctx->metadataBlockHeader.mediaModelLength; } if(ctx->imageInfo.MediaSerialNumber != NULL && ctx->metadataBlockHeader.mediaSerialNumberLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaSerialNumber, ctx->metadataBlockHeader.mediaSerialNumberLength); ctx->metadataBlockHeader.mediaSerialNumberOffset = pos; pos += ctx->metadataBlockHeader.mediaSerialNumberLength; } if(ctx->imageInfo.MediaBarcode != NULL && ctx->metadataBlockHeader.mediaBarcodeLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaBarcode, ctx->metadataBlockHeader.mediaBarcodeLength); ctx->metadataBlockHeader.mediaBarcodeOffset = pos; pos += ctx->metadataBlockHeader.mediaBarcodeLength; } if(ctx->imageInfo.MediaPartNumber != NULL && ctx->metadataBlockHeader.mediaPartNumberLength > 0) { memcpy(buffer + pos, ctx->imageInfo.MediaPartNumber, ctx->metadataBlockHeader.mediaPartNumberLength); ctx->metadataBlockHeader.mediaPartNumberOffset = pos; pos += ctx->metadataBlockHeader.mediaPartNumberLength; } if(ctx->imageInfo.DriveManufacturer != NULL && ctx->metadataBlockHeader.driveManufacturerLength > 0) { memcpy(buffer + pos, ctx->imageInfo.DriveManufacturer, ctx->metadataBlockHeader.driveManufacturerLength); ctx->metadataBlockHeader.driveManufacturerOffset = pos; pos += ctx->metadataBlockHeader.driveManufacturerLength; } if(ctx->imageInfo.DriveModel != NULL && ctx->metadataBlockHeader.driveModelLength > 0) { memcpy(buffer + pos, ctx->imageInfo.DriveModel, ctx->metadataBlockHeader.driveModelLength); ctx->metadataBlockHeader.driveModelOffset = pos; pos += ctx->metadataBlockHeader.driveModelLength; } if(ctx->imageInfo.DriveSerialNumber != NULL && ctx->metadataBlockHeader.driveSerialNumberLength > 0) { memcpy(buffer + pos, ctx->imageInfo.DriveSerialNumber, ctx->metadataBlockHeader.driveSerialNumberLength); ctx->metadataBlockHeader.driveSerialNumberOffset = pos; pos += ctx->metadataBlockHeader.driveSerialNumberLength; } if(ctx->imageInfo.DriveFirmwareRevision != NULL && ctx->metadataBlockHeader.driveFirmwareRevisionLength > 0) { memcpy(buffer + pos, ctx->imageInfo.DriveFirmwareRevision, ctx->metadataBlockHeader.driveFirmwareRevisionLength); ctx->metadataBlockHeader.driveFirmwareRevisionOffset = pos; } fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing metadata block at position %ld", block_position); if(fwrite(buffer, ctx->metadataBlockHeader.blockSize, 1, ctx->imageStream) == 1) { TRACE("Successfully wrote metadata block"); // Add metadata block to index TRACE("Adding metadata block to index"); IndexEntry index_entry; index_entry.blockType = MetadataBlock; index_entry.dataType = 0; index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &index_entry); TRACE("Added metadata block index entry at offset %" PRIu64, block_position); } free(buffer); } /** * @brief Serialize the dump hardware block containing acquisition environment information. * * This function writes a DumpHardwareBlock to the image file, documenting the hardware and software * environments used to create the image. A dump hardware block records one or more "dump environments" – * typically combinations of physical devices (drives, controllers, adapters) and the software stacks * that performed the read operations. This metadata is essential for understanding the imaging context, * validating acquisition integrity, reproducing imaging conditions, and supporting forensic or archival * documentation requirements. * * Each environment entry includes hardware identification (manufacturer, model, revision, firmware, * serial number), software identification (name, version, operating system), and optional extent ranges * that specify which logical sectors or units were contributed by that particular environment. This * structure supports complex imaging scenarios where multiple devices or software configurations were * used to create a composite image. * * The dump hardware block is optional; if no dump hardware information has been populated * (dumpHardwareEntriesWithData is NULL, entries count is zero, or identifier is not set to * DumpHardwareBlock), the function returns immediately without writing anything. This no-op behavior * allows the close operation to proceed gracefully whether or not dump hardware metadata was included * during image creation. * * **Block structure:** * The serialized block consists of: * 1. DumpHardwareHeader (16 bytes: identifier, entries count, payload length, CRC64) * 2. For each dump hardware entry (variable size): * - DumpHardwareEntry (36 bytes: length fields for all strings and extent count) * - Variable-length UTF-8 strings in order: manufacturer, model, revision, firmware, serial, * software name, software version, software operating system * - Array of DumpExtent structures (16 bytes each) if extent count > 0 * * All strings are UTF-8 encoded and NOT null-terminated in the serialized block. String lengths * are measured in bytes, not character counts. * * **Serialization process:** * 1. Allocate a temporary buffer sized to hold the complete block (header + all payload data) * 2. Iterate through all dump hardware entries in ctx->dumpHardwareEntriesWithData * 3. For each entry, copy the DumpHardwareEntry structure followed by each non-NULL string * (only if the corresponding length > 0), then copy the extent array (if extents > 0) * 4. Calculate CRC64-ECMA over the payload (everything after the header) * 5. Copy the DumpHardwareHeader with calculated CRC64 to the beginning of the buffer * 6. Align file position to block boundary * 7. Write the complete buffer to the image file * 8. Add index entry on successful write * 9. Free the temporary buffer * * **Alignment and file positioning:** * Before writing the block, the file position is moved to EOF and then aligned forward to the * next boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived * from ctx->userDataDdtHeader.blockAlignmentShift. This ensures the dump hardware block begins * on a properly aligned offset for efficient I/O and compliance with the Aaru format specification. * * **CRC64 calculation:** * The function calculates CRC64-ECMA over the payload portion of the buffer (everything after * the DumpHardwareHeader) and stores it in the header before writing. This checksum allows * verification of dump hardware block integrity when reading the image. * * **Index registration:** * After successfully writing the complete block, an IndexEntry is appended to ctx->indexEntries with: * - blockType = DumpHardwareBlock * - dataType = 0 (dump hardware blocks have no subtype) * - offset = the aligned file position where the block was written * * **Error handling:** * Memory allocation failures (calloc returning NULL) cause immediate return without writing. * Bounds checking is performed during serialization; if calculated entry sizes exceed the allocated * buffer, the buffer is freed and the function returns without writing. Write errors (fwrite * returning < 1) are silently ignored; no index entry is added if the write fails. Diagnostic * TRACE logs report success or failure. The function does not propagate error codes; higher-level * close logic must validate overall integrity if needed. * * **No-op conditions:** * - ctx->dumpHardwareEntriesWithData is NULL (no hardware data loaded) OR * - ctx->dumpHardwareHeader.entries == 0 (no entries to write) OR * - ctx->dumpHardwareHeader.identifier != DumpHardwareBlock (block not properly initialized) * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->dumpHardwareHeader contains the header with identifier, entry count, and * total payload length. ctx->dumpHardwareEntriesWithData contains the array of * dump hardware entries with their associated string data and extents (may be NULL * if no dump hardware was added). ctx->imageStream must be open and writable. * ctx->indexEntries must be initialized (utarray) to accept new index entries. * * @note Dump Hardware Environments: * - Each entry represents one hardware/software combination used during imaging * - Multiple entries support scenarios where different devices contributed different sectors * - Extent arrays specify which logical sector ranges each environment contributed * - Empty extent arrays (extents == 0) indicate the environment dumped the entire medium * - Overlapping extents between entries may indicate verification passes or redundancy * * @note Hardware Identification Fields: * - manufacturer: Device manufacturer (e.g., "Plextor", "Sony", "Samsung") * - model: Device model number (e.g., "PX-716A", "DRU-820A") * - revision: Hardware revision identifier * - firmware: Firmware version (e.g., "1.11", "KY08") * - serial: Device serial number for unique identification * * @note Software Identification Fields: * - softwareName: Dumping software name (e.g., "Aaru", "ddrescue", "IsoBuster") * - softwareVersion: Software version (e.g., "5.3.0", "1.25") * - softwareOperatingSystem: Host OS (e.g., "Linux 5.10.0", "Windows 10", "macOS 12.0") * * @note String Encoding: * - All strings are UTF-8 encoded * - Strings are NOT null-terminated in the serialized block * - String lengths in DumpHardwareEntry are in bytes, not character counts * - The library maintains null-terminated strings in memory for convenience * - Only non-null-terminated data is written to the file * * @note Memory Management: * - The function allocates a temporary buffer to serialize the entire block * - The buffer is freed before the function returns, regardless of success or failure * - The source data in ctx->dumpHardwareEntriesWithData is not modified * - The source data is freed later during context cleanup (aaruf_close) * * @note Use Cases: * - Forensic documentation requiring complete equipment chain of custody * - Archival metadata for long-term preservation requirements * - Reproducing imaging conditions for verification or re-imaging * - Identifying firmware-specific issues or drive-specific behaviors * - Multi-device imaging scenario documentation * - Correlating imaging artifacts with specific hardware/software combinations * * @note Order in Close Sequence: * - Dump hardware blocks are typically written after sector data but before metadata blocks * - The exact position in the file depends on what other blocks precede it * - The index entry ensures the dump hardware block can be located during subsequent opens * * @warning The temporary buffer allocation may fail on systems with limited memory or when the * dump hardware block is extremely large (many entries with long strings and extents). * Allocation failures result in silent no-op; the image is created without dump hardware. * * @warning Bounds checking during serialization protects against buffer overruns. If calculated * entry sizes exceed the allocated buffer length (which should never occur if the * header's length field is correct), the function aborts without writing. This is a * sanity check against data corruption. * * @see DumpHardwareHeader for the block header structure definition. * @see DumpHardwareEntry for the per-environment entry structure definition. * @see DumpExtent for the extent range structure definition. * @see aaruf_get_dumphw() for retrieving dump hardware from opened images. * @see process_dumphw_block() for the loading process during image opening. * * @internal */ static void write_dumphw_block(aaruformatContext *ctx) { if(ctx->dumpHardwareEntriesWithData == NULL || ctx->dumpHardwareHeader.entries == 0 || ctx->dumpHardwareHeader.identifier != DumpHardwareBlock) return; const size_t required_length = sizeof(DumpHardwareHeader) + ctx->dumpHardwareHeader.length; uint8_t *buffer = calloc(1, required_length); if(buffer == NULL) return; // Start to iterate and copy the data size_t offset = 0; for(int i = 0; i < ctx->dumpHardwareHeader.entries; i++) { size_t entry_size = sizeof(DumpHardwareEntry) + ctx->dumpHardwareEntriesWithData[i].entry.manufacturerLength + ctx->dumpHardwareEntriesWithData[i].entry.modelLength + ctx->dumpHardwareEntriesWithData[i].entry.revisionLength + ctx->dumpHardwareEntriesWithData[i].entry.firmwareLength + ctx->dumpHardwareEntriesWithData[i].entry.serialLength + ctx->dumpHardwareEntriesWithData[i].entry.softwareNameLength + ctx->dumpHardwareEntriesWithData[i].entry.softwareVersionLength + ctx->dumpHardwareEntriesWithData[i].entry.softwareOperatingSystemLength + ctx->dumpHardwareEntriesWithData[i].entry.extents * sizeof(DumpExtent); if(offset + entry_size > required_length) { FATAL("Calculated size exceeds provided buffer length"); free(buffer); return; } memcpy(buffer + offset, &ctx->dumpHardwareEntriesWithData[i].entry, sizeof(DumpHardwareEntry)); offset += sizeof(DumpHardwareEntry); if(ctx->dumpHardwareEntriesWithData[i].entry.manufacturerLength > 0 && ctx->dumpHardwareEntriesWithData[i].manufacturer != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].manufacturer, ctx->dumpHardwareEntriesWithData[i].entry.manufacturerLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.manufacturerLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.modelLength > 0 && ctx->dumpHardwareEntriesWithData[i].model != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].model, ctx->dumpHardwareEntriesWithData[i].entry.modelLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.modelLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.revisionLength > 0 && ctx->dumpHardwareEntriesWithData[i].revision != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].revision, ctx->dumpHardwareEntriesWithData[i].entry.revisionLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.revisionLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.firmwareLength > 0 && ctx->dumpHardwareEntriesWithData[i].firmware != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].firmware, ctx->dumpHardwareEntriesWithData[i].entry.firmwareLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.firmwareLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.serialLength > 0 && ctx->dumpHardwareEntriesWithData[i].serial != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].serial, ctx->dumpHardwareEntriesWithData[i].entry.serialLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.serialLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.softwareNameLength > 0 && ctx->dumpHardwareEntriesWithData[i].softwareName != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].softwareName, ctx->dumpHardwareEntriesWithData[i].entry.softwareNameLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.softwareNameLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.softwareVersionLength > 0 && ctx->dumpHardwareEntriesWithData[i].softwareVersion != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].softwareVersion, ctx->dumpHardwareEntriesWithData[i].entry.softwareVersionLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.softwareVersionLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.softwareOperatingSystemLength > 0 && ctx->dumpHardwareEntriesWithData[i].softwareOperatingSystem != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].softwareOperatingSystem, ctx->dumpHardwareEntriesWithData[i].entry.softwareOperatingSystemLength); offset += ctx->dumpHardwareEntriesWithData[i].entry.softwareOperatingSystemLength; } if(ctx->dumpHardwareEntriesWithData[i].entry.extents > 0 && ctx->dumpHardwareEntriesWithData[i].extents != NULL) { memcpy(buffer + offset, ctx->dumpHardwareEntriesWithData[i].extents, ctx->dumpHardwareEntriesWithData[i].entry.extents * sizeof(DumpExtent)); offset += ctx->dumpHardwareEntriesWithData[i].entry.extents * sizeof(DumpExtent); } } // Calculate CRC64 ctx->dumpHardwareHeader.crc64 = aaruf_crc64_data(buffer + sizeof(DumpHardwareHeader), ctx->dumpHardwareHeader.length); // Copy header memcpy(buffer, &ctx->dumpHardwareHeader, sizeof(DumpHardwareHeader)); fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing dump hardware block at position %ld", block_position); if(fwrite(buffer, required_length, 1, ctx->imageStream) == 1) { TRACE("Successfully wrote dump hardware block"); // Add dump hardware block to index TRACE("Adding dump hardware block to index"); IndexEntry index_entry; index_entry.blockType = DumpHardwareBlock; index_entry.dataType = 0; index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &index_entry); TRACE("Added dump hardware block index entry at offset %" PRIu64, block_position); } free(buffer); } /** * @brief Serialize the CICM XML metadata block to the image file. * * This function writes a CicmBlock containing embedded CICM (Canary Islands Computer Museum) XML * metadata to the Aaru image file. The CICM XML format is a standardized metadata schema used for * documenting preservation and archival information about media and disk images. The XML payload * is stored in its original form without parsing, interpretation, or validation by the library, * preserving the exact structure and content provided during image creation. * * The CICM block is optional; if no CICM metadata has been populated (cicmBlock is NULL, length * is zero, or identifier is not set to CicmBlock), the function returns immediately without * writing anything. This no-op behavior allows the close operation to proceed gracefully whether * or not CICM metadata was included during image creation. * * **Block structure:** * The serialized block consists of: * 1. CicmMetadataBlock header (8 bytes: identifier + length) * 2. Variable-length XML payload: the raw UTF-8 encoded CICM XML data * * The header contains: * - identifier: Always set to CicmBlock (0x4D434943, "CICM" in ASCII) * - length: Size in bytes of the XML payload that immediately follows the header * * **Alignment and file positioning:** * Before writing the block, the file position is moved to EOF and then aligned forward to the * next boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived * from ctx->userDataDdtHeader.blockAlignmentShift. This ensures the CICM block begins on a * properly aligned offset for efficient I/O and compliance with the Aaru format specification. * * **Write sequence:** * The function performs a two-stage write operation: * 1. Write the CicmMetadataBlock header (sizeof(CicmMetadataBlock) bytes) * 2. Write the XML payload (ctx->cicmBlockHeader.length bytes) * * Both writes must succeed for the block to be considered successfully written. If the header * write fails, the payload write is skipped. Only if both writes succeed is an index entry added. * * **Index registration:** * After successfully writing both the header and XML payload, an IndexEntry is appended to * ctx->indexEntries with: * - blockType = CicmBlock * - dataType = 0 (CICM blocks have no subtype) * - offset = the aligned file position where the CicmMetadataBlock header was written * * **Error handling:** * Write errors (fwrite returning < 1) are silently ignored; no index entry is added if either * write fails. Diagnostic TRACE logs report success or failure. The function does not propagate * error codes; higher-level close logic must validate overall integrity if needed. * * **No-op conditions:** * - ctx->cicmBlock is NULL (no XML data loaded) OR * - ctx->cicmBlockHeader.length == 0 (empty metadata) OR * - ctx->cicmBlockHeader.identifier != CicmBlock (block not properly initialized) * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->cicmBlockHeader contains the header with identifier and length fields. * ctx->cicmBlock points to the actual UTF-8 encoded XML data (may be NULL if no * CICM metadata was provided). ctx->imageStream must be open and writable. * ctx->indexEntries must be initialized (utarray) to accept new index entries. * * @note XML Encoding and Format: * - The XML payload is stored in UTF-8 encoding * - The payload may or may not be null-terminated * - The library treats the XML as opaque binary data * - No XML parsing, interpretation, or validation is performed during write * - Schema compliance is the responsibility of the code that set the CICM metadata * * @note CICM Metadata Purpose: * - Developed by the Canary Islands Computer Museum for digital preservation * - Documents comprehensive preservation metadata following a standardized schema * - Includes checksums for data integrity verification * - Records detailed device and media information * - Supports archival and long-term preservation requirements * - Used by cultural heritage institutions and archives * * @note Memory Management: * - The function does not allocate or free any memory * - ctx->cicmBlock memory is managed by the caller (typically freed during aaruf_close) * - The XML data is written directly from the existing buffer * * @note Unlike data blocks (which may be compressed and include CRC64 checksums), the CICM * block is written without compression or explicit integrity checking. The XML payload * is written verbatim as provided, relying on file-level integrity mechanisms. * * @note Order in Close Sequence: * - CICM blocks are typically written after structural data blocks but before the index * - The exact position in the file depends on what other blocks precede it * - The index entry ensures the CICM block can be located during subsequent opens * * @see CicmMetadataBlock for the on-disk structure definition. * @see aaruf_get_cicm_metadata() for retrieving CICM XML from opened images. * * @internal */ static void write_cicm_block(const aaruformatContext *ctx) { if(ctx->cicmBlock == NULL || ctx->cicmBlockHeader.length == 0 || ctx->cicmBlockHeader.identifier != CicmBlock) return; fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing CICM XML block at position %ld", block_position); if(fwrite(&ctx->cicmBlockHeader, sizeof(CicmMetadataBlock), 1, ctx->imageStream) == 1) if(fwrite(ctx->cicmBlock, ctx->cicmBlockHeader.length, 1, ctx->imageStream) == 1) { TRACE("Successfully wrote CICM XML block"); // Add CICM block to index TRACE("Adding CICM XML block to index"); IndexEntry index_entry; index_entry.blockType = CicmBlock; index_entry.dataType = 0; index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &index_entry); TRACE("Added CICM XML block index entry at offset %" PRIu64, block_position); } } /** * @brief Serialize the Aaru metadata JSON block to the image file. * * This function writes an AaruMetadataJsonBlock containing embedded Aaru metadata JSON to the * Aaru image file. The Aaru metadata JSON format is a structured, machine-readable representation * of comprehensive image metadata including media information, imaging session details, hardware * configuration, optical disc tracks and sessions, checksums, and preservation metadata. The JSON * payload is stored in its original form without parsing, interpretation, or validation by the * library, preserving the exact structure and content provided during image creation. * * The Aaru JSON block is optional; if no Aaru JSON metadata has been populated (jsonBlock is NULL, * length is zero, or identifier is not set to AaruMetadataJsonBlock), the function returns * immediately without writing anything. This no-op behavior allows the close operation to proceed * gracefully whether or not Aaru JSON metadata was included during image creation. * * **Block structure:** * The serialized block consists of: * 1. AaruMetadataJsonBlockHeader (8 bytes: identifier + length) * 2. Variable-length JSON payload: the raw UTF-8 encoded Aaru metadata JSON data * * The header contains: * - identifier: Always set to AaruMetadataJsonBlock (0x444D534A, "JSMD" in ASCII) * - length: Size in bytes of the JSON payload that immediately follows the header * * **Alignment and file positioning:** * Before writing the block, the file position is moved to EOF and then aligned forward to the * next boundary satisfying (position & alignment_mask) == 0, where alignment_mask is derived * from ctx->userDataDdtHeader.blockAlignmentShift. This ensures the Aaru JSON block begins on a * properly aligned offset for efficient I/O and compliance with the Aaru format specification. * * **Write sequence:** * The function performs a two-stage write operation: * 1. Write the AaruMetadataJsonBlockHeader (sizeof(AaruMetadataJsonBlockHeader) bytes) * 2. Write the JSON payload (ctx->jsonBlockHeader.length bytes) * * Both writes must succeed for the block to be considered successfully written. If the header * write fails, the payload write is skipped. Only if both writes succeed is an index entry added. * * **Index registration:** * After successfully writing both the header and JSON payload, an IndexEntry is appended to * ctx->indexEntries with: * - blockType = AaruMetadataJsonBlock * - dataType = 0 (Aaru JSON blocks have no subtype) * - offset = the aligned file position where the AaruMetadataJsonBlockHeader was written * * **Error handling:** * Write errors (fwrite returning < 1) are silently ignored; no index entry is added if either * write fails. Diagnostic TRACE logs report success or failure. The function does not propagate * error codes; higher-level close logic must validate overall integrity if needed. * * **No-op conditions:** * - ctx->jsonBlock is NULL (no JSON data loaded) OR * - ctx->jsonBlockHeader.length == 0 (empty metadata) OR * - ctx->jsonBlockHeader.identifier != AaruMetadataJsonBlock (block not properly initialized) * * @param ctx Pointer to an initialized aaruformatContext in write mode. Must not be NULL. * ctx->jsonBlockHeader contains the header with identifier and length fields. * ctx->jsonBlock points to the actual UTF-8 encoded JSON data (may be NULL if no * Aaru JSON metadata was provided). ctx->imageStream must be open and writable. * ctx->indexEntries must be initialized (utarray) to accept new index entries. * * @note JSON Encoding and Format: * - The JSON payload is stored in UTF-8 encoding * - The payload may or may not be null-terminated * - The library treats the JSON as opaque binary data * - No JSON parsing, interpretation, or validation is performed during write * - Schema compliance is the responsibility of the code that set the Aaru JSON metadata * * @note Aaru Metadata JSON Purpose: * - Provides machine-readable structured metadata using modern JSON format * - Includes comprehensive information about media, sessions, tracks, and checksums * - Enables programmatic access to metadata without XML parsing overhead * - Documents imaging session details, hardware configuration, and preservation data * - Used by Aaru and compatible tools for metadata exchange and analysis * - Complements or serves as alternative to CICM XML metadata * * @note Memory Management: * - The function does not allocate or free any memory * - ctx->jsonBlock memory is managed by the caller (typically freed during aaruf_close) * - The JSON data is written directly from the existing buffer * * @note Unlike data blocks (which may be compressed and include CRC64 checksums), the Aaru JSON * block is written without compression or explicit integrity checking. The JSON payload * is written verbatim as provided, relying on file-level integrity mechanisms. * * @note Order in Close Sequence: * - Aaru JSON blocks are typically written after CICM blocks but before the index * - The exact position in the file depends on what other blocks precede it * - The index entry ensures the Aaru JSON block can be located during subsequent opens * * @note Distinction from CICM XML: * - Both CICM XML and Aaru JSON blocks can be written to the same image * - CICM XML follows the Canary Islands Computer Museum schema (older format) * - Aaru JSON follows the Aaru-specific metadata schema (newer format) * - They serve similar purposes but with different structures and consumers * - Including both provides maximum compatibility across different tools * * @see AaruMetadataJsonBlockHeader for the on-disk structure definition. * @see aaruf_set_aaru_json_metadata() for setting Aaru JSON during image creation. * @see aaruf_get_aaru_json_metadata() for retrieving Aaru JSON from opened images. * @see write_cicm_block() for the similar function that writes CICM XML blocks. * * @internal */ static void write_aaru_json_block(const aaruformatContext *ctx) { if(ctx->jsonBlock == NULL || ctx->jsonBlockHeader.length == 0 || ctx->jsonBlockHeader.identifier != AaruMetadataJsonBlock) return; fseek(ctx->imageStream, 0, SEEK_END); long block_position = ftell(ctx->imageStream); const uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(block_position & alignment_mask) { const uint64_t aligned_position = block_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); block_position = aligned_position; } TRACE("Writing Aaru metadata JSON block at position %ld", block_position); if(fwrite(&ctx->jsonBlockHeader, sizeof(AaruMetadataJsonBlockHeader), 1, ctx->imageStream) == 1) if(fwrite(ctx->jsonBlock, ctx->jsonBlockHeader.length, 1, ctx->imageStream) == 1) { TRACE("Successfully wrote Aaru metadata JSON block"); // Add Aaru metadata JSON block to index TRACE("Adding Aaru metadata JSON block to index"); IndexEntry index_entry; index_entry.blockType = AaruMetadataJsonBlock; index_entry.dataType = 0; index_entry.offset = block_position; utarray_push_back(ctx->indexEntries, &index_entry); TRACE("Added Aaru metadata JSON block index entry at offset %" PRIu64, block_position); } } /** * @brief Serialize the accumulated index entries at the end of the image and back-patch the header. * * All previously written structural blocks push their IndexEntry into ctx->indexEntries. This * function collects them, writes an IndexHeader3 followed by each IndexEntry, computes CRC64 over * the entries, and then updates the main AaruHeaderV2 (at offset 0) with the index offset. The * index itself is aligned to the DDT block boundary. No previous index chaining is currently * implemented (index_header.previous = 0). * * @param ctx Pointer to an initialized aaruformatContext in write mode. * @return AARUF_STATUS_OK on success; AARUF_ERROR_CANNOT_WRITE_HEADER if the index header, any * entry, or the header back-patch fails. * @retval AARUF_STATUS_OK Index written and header updated. * @retval AARUF_ERROR_CANNOT_WRITE_HEADER Failed writing index header, entries, or updating main header. * @internal */ static int32_t write_index_block(aaruformatContext *ctx) { // Write the complete index at the end of the file TRACE("Writing index at the end of the file"); fseek(ctx->imageStream, 0, SEEK_END); long index_position = ftell(ctx->imageStream); // Align index position to block boundary if needed uint64_t alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; if(index_position & alignment_mask) { uint64_t aligned_position = index_position + alignment_mask & ~alignment_mask; fseek(ctx->imageStream, aligned_position, SEEK_SET); index_position = aligned_position; TRACE("Aligned index position to %" PRIu64, aligned_position); } // Prepare index header IndexHeader3 index_header; index_header.identifier = IndexBlock3; index_header.entries = utarray_len(ctx->indexEntries); index_header.previous = 0; // No previous index for now TRACE("Writing index with %" PRIu64 " entries at position %ld", index_header.entries, index_position); // Calculate CRC64 of index entries crc64_ctx *index_crc64_context = aaruf_crc64_init(); if(index_crc64_context != NULL && index_header.entries > 0) { size_t index_data_size = index_header.entries * sizeof(IndexEntry); aaruf_crc64_update(index_crc64_context, (uint8_t *)utarray_front(ctx->indexEntries), index_data_size); aaruf_crc64_final(index_crc64_context, &index_header.crc64); TRACE("Calculated index CRC64: 0x%16lX", index_header.crc64); } else index_header.crc64 = 0; // Write index header if(fwrite(&index_header, sizeof(IndexHeader3), 1, ctx->imageStream) == 1) { TRACE("Successfully wrote index header"); // Write index entries if(index_header.entries > 0) { size_t entries_written = 0; IndexEntry *entry = NULL; for(entry = (IndexEntry *)utarray_front(ctx->indexEntries); entry != NULL; entry = (IndexEntry *)utarray_next(ctx->indexEntries, entry)) if(fwrite(entry, sizeof(IndexEntry), 1, ctx->imageStream) == 1) { entries_written++; TRACE("Wrote index entry: blockType=0x%08X dataType=%u offset=%" PRIu64, entry->blockType, entry->dataType, entry->offset); } else { TRACE("Failed to write index entry %zu", entries_written); break; } if(entries_written == index_header.entries) { TRACE("Successfully wrote all %zu index entries", entries_written); // Update header with index offset and rewrite it ctx->header.indexOffset = index_position; TRACE("Updating header with index offset: %" PRIu64, ctx->header.indexOffset); // Seek back to beginning and rewrite header fseek(ctx->imageStream, 0, SEEK_SET); if(fwrite(&ctx->header, sizeof(AaruHeaderV2), 1, ctx->imageStream) == 1) TRACE("Successfully updated header with index offset"); else { TRACE("Failed to update header with index offset"); return AARUF_ERROR_CANNOT_WRITE_HEADER; } } else { TRACE("Failed to write all index entries (wrote %zu of %" PRIu64 ")", entries_written, index_header.entries); return AARUF_ERROR_CANNOT_WRITE_HEADER; } } } else { TRACE("Failed to write index header"); return AARUF_ERROR_CANNOT_WRITE_HEADER; } return AARUF_STATUS_OK; } /** * @brief Close an Aaru image context, flushing pending data structures and releasing resources. * * Public API entry point used to finalize an image being written or simply dispose of a context * opened for reading. For write-mode contexts (ctx->isWriting true) the function performs the * following ordered steps: * 1. Rewrite the (possibly updated) main header at offset 0. * 2. Close any open data block via aaruf_close_current_block(). * 3. Flush a cached secondary DDT (multi-level) if pending. * 4. Flush either the primary DDT (multi-level) or the single-level DDT table. * 5. Finalize and append checksum block(s) for all enabled algorithms. * 6. Write auxiliary metadata blocks: tracks, MODE 2 subheaders, sector prefix. * 7. Serialize the global index and patch header.indexOffset. * 8. Clear deduplication hash map if used. * * Afterwards (or for read-mode contexts) all dynamically allocated buffers, arrays, hash tables * and mapping structures are freed/unmapped. Media tags are removed from their hash table. * * Error Handling: * - Returns -1 with errno = EINVAL if the provided pointer is NULL or not a valid context. * - Returns -1 with errno set to AARUF_ERROR_CANNOT_WRITE_HEADER if a header write fails. * - If any intermediate serialization helper returns an error status, that error value is * propagated (converted to -1 with errno set accordingly by the caller if desired). In the * current implementation aaruf_close() directly returns the negative error code for helper * failures to preserve detail. * * @param context Opaque pointer returned by earlier open/create calls (must be an aaruformatContext). * @return 0 on success; -1 or negative libaaruformat error code on failure. * @retval 0 All pending data flushed (if writing) and resources released successfully. * @retval -1 Invalid context pointer or initial header rewrite failure (errno = EINVAL or * AARUF_ERROR_CANNOT_WRITE_HEADER). * @retval AARUF_ERROR_CANNOT_WRITE_HEADER A later write helper (e.g., index, DDT) failed and returned this code * directly. * @retval Propagated from a write helper if future helpers add more error codes. * @note On success the context memory itself is freed; the caller must not reuse the pointer. */ int aaruf_close(void *context) { TRACE("Entering aaruf_close(%p)", context); mediaTagEntry *media_tag = NULL; mediaTagEntry *tmp_media_tag = NULL; if(context == NULL) { FATAL("Invalid context"); errno = EINVAL; return -1; } aaruformatContext *ctx = context; // Not a libaaruformat context if(ctx->magic != AARU_MAGIC) { FATAL("Invalid context"); errno = EINVAL; return -1; } if(ctx->isWriting) { TRACE("File is writing"); TRACE("Seeking to start of image"); // Write the header at the beginning of the file fseek(ctx->imageStream, 0, SEEK_SET); TRACE("Writing header at position 0"); if(fwrite(&ctx->header, sizeof(AaruHeaderV2), 1, ctx->imageStream) != 1) { fclose(ctx->imageStream); ctx->imageStream = NULL; errno = AARUF_ERROR_CANNOT_WRITE_HEADER; return -1; } // Close current block first TRACE("Closing current block if any"); if(ctx->writingBuffer != NULL) { int error = aaruf_close_current_block(ctx); if(error != AARUF_STATUS_OK) return error; } // Write cached secondary DDT table if any int32_t res = write_cached_secondary_ddt(ctx); if(res != AARUF_STATUS_OK) return res; // Write primary DDT table (multi-level) if applicable res = write_primary_ddt(ctx); if(res != AARUF_STATUS_OK) return res; // Write single-level DDT table if applicable res = write_single_level_ddt(ctx); if(res != AARUF_STATUS_OK) return res; // Finalize checksums and write checksum block write_checksum_block(ctx); // Write tracks block write_tracks_block(ctx); // Write MODE 2 subheader data block write_mode2_subheaders_block(ctx); // Write CD sector prefix data block write_sector_prefix(ctx); // Write sector prefix DDT (statuses + optional indexes) write_sector_prefix_ddt(ctx); // Write CD sector suffix data block (EDC/ECC captures) write_sector_suffix(ctx); // Write sector prefix DDT (EDC/ECC captures) write_sector_suffix_ddt(ctx); // Write sector subchannel data block write_sector_subchannel(ctx); // Write DVD long sector data blocks write_dvd_long_sector_blocks(ctx); // Write DVD decrypted title keys write_dvd_title_key_decrypted_block(ctx); // Write media tags data blocks write_media_tags(ctx); // Write geometry block if any write_geometry_block(ctx); // Write metadata block write_metadata_block(ctx); // Write dump hardware block if any write_dumphw_block(ctx); // Write CICM XML block if any write_cicm_block(ctx); // Write Aaru metadata JSON block if any write_aaru_json_block(ctx); // Write the complete index at the end of the file res = write_index_block(ctx); if(res != AARUF_STATUS_OK) return res; if(ctx->deduplicate && ctx->sectorHashMap != NULL) { TRACE("Clearing sector hash map"); // Clear sector hash map free_map(ctx->sectorHashMap); ctx->sectorHashMap = NULL; } } TRACE("Freeing memory pointers"); // This may do nothing if imageStream is NULL, but as the behaviour is undefined, better sure than sorry if(ctx->imageStream != NULL) { fclose(ctx->imageStream); ctx->imageStream = NULL; } // Free index entries array if(ctx->indexEntries != NULL) { utarray_free(ctx->indexEntries); ctx->indexEntries = NULL; } free(ctx->sector_prefix); ctx->sector_prefix = NULL; free(ctx->sectorPrefixCorrected); ctx->sectorPrefixCorrected = NULL; free(ctx->sector_suffix); ctx->sector_suffix = NULL; free(ctx->sectorSuffixCorrected); ctx->sectorSuffixCorrected = NULL; free(ctx->sector_subchannel); ctx->sector_subchannel = NULL; free(ctx->mode2_subheaders); ctx->mode2_subheaders = NULL; TRACE("Freeing media tags"); if(ctx->mediaTags != NULL) HASH_ITER(hh, ctx->mediaTags, media_tag, tmp_media_tag) { HASH_DEL(ctx->mediaTags, media_tag); free(media_tag->data); free(media_tag); } #ifdef __linux__ // TODO: Implement TRACE("Unmapping user data DDT if it is not in memory"); if(!ctx->inMemoryDdt) { munmap(ctx->userDataDdt, ctx->mappedMemoryDdtSize); ctx->userDataDdt = NULL; } #endif free(ctx->sectorPrefixDdtMini); ctx->sectorPrefixDdtMini = NULL; free(ctx->sectorPrefixDdt); ctx->sectorPrefixDdt = NULL; free(ctx->sectorSuffixDdtMini); ctx->sectorSuffixDdtMini = NULL; free(ctx->sectorSuffixDdt); ctx->sectorSuffixDdt = NULL; free(ctx->metadataBlock); ctx->metadataBlock = NULL; free(ctx->trackEntries); ctx->trackEntries = NULL; free(ctx->cicmBlock); ctx->cicmBlock = NULL; if(ctx->dumpHardwareEntriesWithData != NULL) { for(int i = 0; i < ctx->dumpHardwareHeader.entries; i++) { free(ctx->dumpHardwareEntriesWithData[i].extents); ctx->dumpHardwareEntriesWithData[i].extents = NULL; free(ctx->dumpHardwareEntriesWithData[i].manufacturer); ctx->dumpHardwareEntriesWithData[i].manufacturer = NULL; free(ctx->dumpHardwareEntriesWithData[i].model); ctx->dumpHardwareEntriesWithData[i].model = NULL; free(ctx->dumpHardwareEntriesWithData[i].revision); ctx->dumpHardwareEntriesWithData[i].revision = NULL; free(ctx->dumpHardwareEntriesWithData[i].firmware); ctx->dumpHardwareEntriesWithData[i].firmware = NULL; free(ctx->dumpHardwareEntriesWithData[i].serial); ctx->dumpHardwareEntriesWithData[i].serial = NULL; free(ctx->dumpHardwareEntriesWithData[i].softwareName); ctx->dumpHardwareEntriesWithData[i].softwareName = NULL; free(ctx->dumpHardwareEntriesWithData[i].softwareVersion); ctx->dumpHardwareEntriesWithData[i].softwareVersion = NULL; free(ctx->dumpHardwareEntriesWithData[i].softwareOperatingSystem); ctx->dumpHardwareEntriesWithData[i].softwareOperatingSystem = NULL; } ctx->dumpHardwareEntriesWithData = NULL; } free(ctx->readableSectorTags); ctx->readableSectorTags = NULL; free(ctx->eccCdContext); ctx->eccCdContext = NULL; free(ctx->checksums.spamsum); ctx->checksums.spamsum = NULL; free(ctx->sector_id); free(ctx->sector_ied); free(ctx->sector_cpr_mai); free(ctx->sector_edc); // TODO: Free caches free(context); TRACE("Exiting aaruf_close() = 0"); return 0; }