diff --git a/src/close.c b/src/close.c index 01f4331..8133bdd 100644 --- a/src/close.c +++ b/src/close.c @@ -15,6 +15,24 @@ * 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 @@ -30,138 +48,878 @@ #include "log.h" /** - * @brief Closes an AaruFormat image context and frees resources. + * @brief Flush a cached secondary (child) DeDuplication Table (DDT) to the image. * - * Closes the image file, frees memory, and releases all resources associated with the context. - * For images opened in write mode, this function performs critical finalization operations - * including writing cached DDT tables, updating the index, writing the final image header, - * and ensuring all data structures are properly persisted. It handles both single-level - * and multi-level DDT structures and performs comprehensive cleanup of all allocated resources. + * 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. * - * @param context Pointer to the aaruformat context to close. + * CRC64 is computed for the serialized table contents and stored in both crc64 and cmpCrc64 + * fields of the written DdtHeader2 (no compression is applied). * - * @return Returns one of the following status codes: - * @retval 0 Successfully closed the context and freed all resources. This is returned when: - * - The context is valid and properly initialized - * - For write mode: All cached data is successfully written to the image file - * - DDT tables (single-level or multi-level) are successfully written and indexed - * - The image index is successfully written with proper CRC validation - * - The final image header is updated with correct index offset and written - * - All memory resources are successfully freed - * - The image stream is closed without errors + * On return the cached secondary table buffers and bookkeeping fields (cachedSecondaryDdtSmall, + * cachedSecondaryDdtBig, cachedDdtOffset) are cleared. * - * @retval -1 Context validation failed. This occurs when: - * - The context parameter is NULL - * - The context magic number doesn't match AARU_MAGIC (invalid context type) - * - The errno is set to EINVAL to indicate invalid argument + * @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. * - * @retval AARUF_ERROR_CANNOT_WRITE_HEADER (-16) Write operations failed. This occurs when: - * - Cannot write the initial image header at position 0 (for write mode) - * - Cannot write cached secondary DDT header or data to the image file - * - Cannot write primary DDT header or table data to the image file - * - Cannot write single-level DDT header or table data to the image file - * - Cannot write index header to the image file - * - Cannot write all index entries to the image file - * - Cannot update the final image header with the correct index offset - * - Any file I/O operation fails during the finalization process + * 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. * - * @retval Error codes from aaruf_close_current_block() may be propagated when: - * - The current writing buffer cannot be properly closed and written - * - Block finalization fails during the close operation - * - Compression or writing of the final block encounters errors + * @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. * - * @note Write Mode Finalization Process: - * - Writes the image header at the beginning of the file - * - Closes any open writing buffer (current block being written) - * - Writes cached secondary DDT tables and updates primary DDT references - * - Writes primary DDT tables (single-level or multi-level) with CRC validation - * - Writes the complete index with all block references at the end of the file - * - Updates the image header with the final index offset + * 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. * - * @note DDT Finalization: - * - For multi-level DDTs: Writes cached secondary tables and updates primary table pointers - * - For single-level DDTs: Writes the complete table directly to the designated position - * - Calculates and validates CRC64 checksums for all DDT data - * - Updates index entries to reference newly written DDT blocks + * @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. * - * @note Index Writing: - * - Creates IndexHeader3 structure with entry count and CRC validation - * - Writes all IndexEntry structures sequentially after the header - * - Aligns index position to block boundaries for optimal access - * - Updates the main image header with the final index file offset + * 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. * - * @note Memory Cleanup: - * - Frees all allocated DDT tables (userDataDdtMini, userDataDdtBig, cached secondary tables) - * - Frees sector metadata arrays (sectorPrefix, sectorSuffix, sectorSubchannel, etc.) - * - Frees media tag hash table and all associated tag data - * - Frees track entries, metadata blocks, and hardware information - * - Closes LRU caches for block headers and data - * - Unmaps memory-mapped DDT structures (Linux-specific) + * 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. * - * @note Platform-Specific Operations: - * - Linux: Unmaps memory-mapped DDT structures using munmap() if not loaded in memory - * - Other platforms: Standard memory deallocation only + * @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. * - * @note Error Handling Strategy: - * - Critical write failures return immediately with error codes - * - Memory cleanup continues even if some write operations fail - * - All allocated resources are freed regardless of write success/failure - * - File stream is always closed, even on error conditions + * 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. * - * @warning This function must be called to properly finalize write-mode images. - * Failing to call aaruf_close() on write-mode contexts will result in - * incomplete or corrupted image files. + * @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. * - * @warning After calling this function, the context pointer becomes invalid and - * should not be used. All operations on the context will result in - * undefined behavior. + * 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. * - * @warning For write-mode contexts, this function performs extensive file I/O. - * Ensure sufficient disk space and proper file permissions before calling. + * @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. * - * @warning The function sets errno to EINVAL for context validation failures - * but uses library-specific error codes for write operation failures. + * 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. * - * @note Checksum Block Writing: - * - Finalizes any active checksum calculations (MD5, SHA1, SHA256, SpamSum, BLAKE3) - * - Builds a ChecksumHeader followed by one or more ChecksumEntry records - * - Each checksum entry stores its algorithm type, length, and raw digest/value - * - The entire checksum block is aligned to the same block boundary policy as other appended blocks - * - Adds a corresponding index entry (blockType = ChecksumBlock) - * - Sets header.featureCompatible |= AARU_FEATURE_RW_BLAKE3 when a BLAKE3 digest is written + * 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. * - * @note Tracks Block Writing: - * - When track metadata (tracksHeader.entries > 0) is present, writes a TracksHeader plus all TrackEntry items - * - Aligns the block prior to writing if required by blockAlignmentShift - * - Appends an index entry (blockType = TracksBlock) + * @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 accumulated index entries at the end of the image and back-patch the header. * - * @note Alignment Strategy: - * - Secondary DDT tables, checksum block, tracks block, and final index are all aligned to 2^blockAlignmentShift - * by padding forward to the next boundary when the current file position is unaligned - * - Alignment ensures predictable block addressing for deduplication and fast random access + * 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). * - * @note Deduplication Hash Map Cleanup: - * - If ctx->deduplicate is true and sectorHashMap exists, free_map() is invoked to release the hash map before - * final context deallocation + * @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. * - * @note Feature Flags: - * - BLAKE3 presence triggers setting the AARU_FEATURE_RW_BLAKE3 bit in the image header's featureCompatible mask + * 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. * - * @note Index CRC (Clarification): - * - A CRC64 is computed over the contiguous array of IndexEntry structures and stored in IndexHeader3.crc64; - * zero is stored if no entries exist or CRC context allocation fails + * 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. * - * @note Error Propagation (Clarification): - * - Failures after partially writing certain optional blocks (checksum, tracks) do not prevent subsequent memory - * cleanup, but may still return AARUF_ERROR_CANNOT_WRITE_HEADER if a critical header/table write fails + * 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. * - * @note Resource Ownership (Clarification): - * - All dynamically allocated checksum buffers (e.g., SpamSum string) and cryptographic contexts are finalized - * or freed prior to context memory release - * - * @note Mode 2 Subheaders Block: - * - If mode2_subheaders is present it is serialized as a DataBlock with type CompactDiscMode2Subheader, CRC64 - * protected and indexed similarly to other appended metadata blocks. + * @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) { @@ -213,688 +971,33 @@ int aaruf_close(void *context) if(error != AARUF_STATUS_OK) return error; } - // 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); + // Write cached secondary DDT table if any + int32_t res = write_cached_secondary_ddt(ctx); + if(res != AARUF_STATUS_OK) return res; - if(has_cached_secondary_ddt) - { - TRACE("Writing cached secondary DDT table to file"); + // Write primary DDT table (multi-level) if applicable + res = write_primary_ddt(ctx); + if(res != AARUF_STATUS_OK) return res; - fseek(ctx->imageStream, 0, SEEK_END); - long end_of_file = ftell(ctx->imageStream); + // Write single-level DDT table if applicable + res = write_single_level_ddt(ctx); + if(res != AARUF_STATUS_OK) return res; - // 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); - } - - // Write the cached primary DDT table back to its position in the file - if(ctx->userDataDdtHeader.tableShift > 0 && (ctx->userDataDdtMini != NULL || ctx->userDataDdtBig != NULL)) - { - 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"); - } - - // Write the single level DDT table block aligned just after the header - if(ctx->userDataDdtHeader.tableShift == 0 && (ctx->userDataDdtMini != NULL || ctx->userDataDdtBig != NULL)) - { - 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"); - } - - 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) - { - 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); - } + // Finalize checksums and write checksum block + write_checksum_block(ctx); // Write tracks block - if(ctx->tracksHeader.entries > 0 && ctx->trackEntries != NULL) - { - fseek(ctx->imageStream, 0, SEEK_END); - long tracks_position = ftell(ctx->imageStream); - // Align index position to block boundary if needed - alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; - if(tracks_position & alignment_mask) - { - 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); - } - } - } + write_tracks_block(ctx); // Write MODE 2 subheader data block - if(ctx->mode2_subheaders != NULL) - { - fseek(ctx->imageStream, 0, SEEK_END); - long mode2_subheaders_position = ftell(ctx->imageStream); - // Align index position to block boundary if needed - alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; - if(mode2_subheaders_position & alignment_mask) - { - aligned_position = mode2_subheaders_position + alignment_mask & ~alignment_mask; - fseek(ctx->imageStream, aligned_position, SEEK_SET); - mode2_subheaders_position = aligned_position; - } + write_mode2_subheaders_block(ctx); - 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); - 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); - } - } - } - - if(ctx->sector_prefix != NULL) - { - fseek(ctx->imageStream, 0, SEEK_END); - long prefix_position = ftell(ctx->imageStream); - // Align index position to block boundary if needed - alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; - if(prefix_position & alignment_mask) - { - 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 subheaders 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 MODE 2 subheaders block index entry at offset %" PRIu64, prefix_position); - } - } - } + // Write CD sector prefix data block + write_sector_prefix(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 - alignment_mask = (1ULL << ctx->userDataDdtHeader.blockAlignmentShift) - 1; - if(index_position & alignment_mask) - { - 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; - } + res = write_index_block(ctx); + if(res != AARUF_STATUS_OK) return res; if(ctx->deduplicate && ctx->sectorHashMap != NULL) {