libaaruformat 1.0 Aaru Data Preservation Suite - Format Library

Loading...
Searching...
No Matches
close.c File Reference

Implements image finalization and resource cleanup for libaaruformat. More...

`#include <errno.h>`
`#include <stdio.h>`
`#include <stdlib.h>`
`#include <aaruformat.h>`
`#include "internal.h"`
`#include "log.h"`

Go to the source code of this file.

## Functions
static int32_t | write_cached_secondary_ddt (aaruformat_context *ctx)
| Flush a cached secondary (child) DeDuplication Table (DDT) to the image.
static int32_t | write_primary_ddt (aaruformat_context *ctx)
| Write (flush) the multi-level primary DDT table header and data back to its file offset.
static int32_t | write_single_level_ddt (aaruformat_context *ctx)
| Serialize a single-level DDT (tableShift == 0) directly after its header.
static int32_t | write_tape_ddt (aaruformat_context *ctx)
| Converts tape DDT hash table to array format and writes it as a single-level DDT.
static void | write_checksum_block (aaruformat_context *ctx)
| Finalize any active checksum calculations and append a checksum block.
static void | write_tracks_block (aaruformat_context *ctx)
| Serialize the tracks metadata block and add it to the index.
static void | write_mode2_subheaders_block (aaruformat_context *ctx)
| Serialize a MODE 2 (XA) subheaders data block.
static void | write_sector_prefix (aaruformat_context *ctx)
| Serialize the optional CD sector prefix block.
static void | write_sector_suffix (aaruformat_context *ctx)
| Serialize the optional CD sector suffix block (EDC/ECC region capture).
static void | write_sector_prefix_ddt (aaruformat_context *ctx)
| Serialize the per-sector CD prefix status / index DeDuplication Table (DDT v2, prefix variant).
static void | write_sector_suffix_ddt (aaruformat_context *ctx)
| Serialize the per-sector CD suffix status / index DeDuplication Table (DDT v2, suffix variant).
static void | write_sector_subchannel (const aaruformat_context *ctx)
| Serialize the per-sector subchannel or tag data block.
void | write_dvd_long_sector_blocks (aaruformat_context *ctx)
| Serialize DVD long sector auxiliary data blocks to the image file.
static void | write_dvd_title_key_decrypted_block (const aaruformat_context *ctx)
| Serialize the DVD decrypted title key data block to the image file.
static void | write_media_tags (const aaruformat_context *ctx)
| Serialize all accumulated media tags to the image file.
static void | write_tape_file_block (const aaruformat_context *ctx)
| Serialize the tape file metadata block to the image file.
static void | write_tape_partition_block (const aaruformat_context *ctx)
| Serialize the tape partition metadata block to the image file.
static void | write_geometry_block (const aaruformat_context *ctx)
| Serialize the geometry metadata block to the image file.
static void | write_metadata_block (aaruformat_context *ctx)
| Serialize the metadata block containing image and media descriptive information.
static void | write_dumphw_block (aaruformat_context *ctx)
| Serialize the dump hardware block containing acquisition environment information.
static void | write_cicm_block (const aaruformat_context *ctx)
| Serialize the CICM XML metadata block to the image file.
static void | write_aaru_json_block (const aaruformat_context *ctx)
| Serialize the Aaru metadata JSON block to the image file.
static int32_t | write_index_block (aaruformat_context *ctx)
| Serialize the accumulated index entries at the end of the image and back-patch the header.
int | aaruf_close (void *context)
| Close an Aaru image context, flushing pending data structures and releasing resources.

## Detailed Description

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.

Definition in file close.c.

## Function Documentation

## ◆ aaruf_close()

int aaruf_close | ( | void * | context | ) |

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.

Parameters

  context | Opaque pointer returned by earlier open/create calls (must be an aaruformatContext).

Returns
  0 on success; -1 or negative libaaruformat error code on failure.

Return values

  0 | All pending data flushed (if writing) and resources released successfully.
  -1 | Invalid context pointer or initial header rewrite failure (errno = EINVAL or AARUF_ERROR_CANNOT_WRITE_HEADER).
  AARUF_ERROR_CANNOT_WRITE_HEADER | A later write helper (e.g., index, DDT) failed and returned this code directly.
  <other | negative libaaruformat code> 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.

Definition at line 3997 of file close.c.

References AARU_CALL, AARU_EXPORT, AARU_MAGIC, aaruf_close_current_block(), AARUF_ERROR_CANNOT_WRITE_HEADER, AARUF_STATUS_OK, aaruformat_context::checksums, aaruformat_context::cicm_block, mediaTagEntry::data, aaruformat_context::deduplicate, aaruformat_context::dump_hardware_entries_with_data, aaruformat_context::dump_hardware_header, aaruformat_context::ecc_cd_context, DumpHardwareHeader::entries, DumpHardwareEntriesWithData::extents, FATAL, DumpHardwareEntriesWithData::firmware, free_map(), aaruformat_context::header, aaruformat_context::imageStream, aaruformat_context::in_memory_ddt, aaruformat_context::index_entries, aaruformat_context::is_tape, aaruformat_context::is_writing, aaruformat_context::magic, DumpHardwareEntriesWithData::manufacturer, aaruformat_context::mapped_memory_ddt_size, aaruformat_context::mediaTags, aaruformat_context::metadata_block, aaruformat_context::mode2_subheaders, DumpHardwareEntriesWithData::model, aaruformat_context::readableSectorTags, DumpHardwareEntriesWithData::revision, aaruformat_context::sector_cpr_mai, aaruformat_context::sector_edc, aaruformat_context::sector_hash_map, aaruformat_context::sector_id, aaruformat_context::sector_ied, aaruformat_context::sector_prefix, aaruformat_context::sector_prefix_corrected, aaruformat_context::sector_prefix_ddt, aaruformat_context::sector_prefix_ddt2, aaruformat_context::sector_subchannel, aaruformat_context::sector_suffix, aaruformat_context::sector_suffix_corrected, aaruformat_context::sector_suffix_ddt, aaruformat_context::sector_suffix_ddt2, DumpHardwareEntriesWithData::serial, DumpHardwareEntriesWithData::softwareName, DumpHardwareEntriesWithData::softwareOperatingSystem, DumpHardwareEntriesWithData::softwareVersion, Checksums::spamsum, TRACE, aaruformat_context::track_entries, aaruformat_context::user_data_ddt, write_aaru_json_block(), write_cached_secondary_ddt(), write_checksum_block(), write_cicm_block(), write_dumphw_block(), write_dvd_long_sector_blocks(), write_dvd_title_key_decrypted_block(), write_geometry_block(), write_index_block(), write_media_tags(), write_metadata_block(), write_mode2_subheaders_block(), write_primary_ddt(), write_sector_prefix(), write_sector_prefix_ddt(), write_sector_subchannel(), write_sector_suffix(), write_sector_suffix_ddt(), write_single_level_ddt(), write_tape_ddt(), write_tape_file_block(), write_tape_partition_block(), write_tracks_block(), and aaruformat_context::writing_buffer.

Referenced by aaruf_open().

## ◆ write_aaru_json_block()

void write_aaru_json_block | ( | const aaruformat_context * | ctx | ) |
| static

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)

Parameters

  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
  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
  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
  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.
  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
  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 also
  AaruMetadataJsonBlockHeader for the on-disk structure definition.
  aaruf_set_aaru_json_metadata() for setting Aaru JSON during image creation.
  aaruf_get_aaru_json_metadata() for retrieving Aaru JSON from opened images.
  write_cicm_block() for the similar function that writes CICM XML blocks.

Definition at line 3814 of file close.c.

References AaruMetadataJsonBlock, DdtHeader2::blockAlignmentShift, IndexEntry::blockType, IndexEntry::dataType, AaruMetadataJsonBlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, aaruformat_context::json_block, aaruformat_context::json_block_header, AaruMetadataJsonBlockHeader::length, IndexEntry::offset, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_cached_secondary_ddt()

int32_t write_cached_secondary_ddt | ( | aaruformat_context * | ctx | ) |
| static

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 crc64; cmpCrc64 stores the checksum of compressed data or equals crc64 if compression is not applied or not effective.

On return the cached secondary table buffers and bookkeeping fields (cachedSecondaryDdtSmall, cachedSecondaryDdtBig, cachedDdtOffset) are cleared.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Returns
  AARUF_STATUS_OK on success, or AARUF_ERROR_CANNOT_WRITE_HEADER if the secondary table or updated primary table cannot be flushed.

Return values

  AARUF_STATUS_OK | Success or no cached secondary DDT needed flushing.
  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.

Definition at line 77 of file close.c.

References aaruf_crc64_data(), aaruf_crc64_final(), aaruf_crc64_init(), aaruf_crc64_update(), AARUF_ERROR_CANNOT_WRITE_HEADER, AARUF_ERROR_NOT_ENOUGH_MEMORY, aaruf_lzma_encode_buffer(), AARUF_STATUS_OK, DdtHeader2::blockAlignmentShift, DdtHeader2::blocks, IndexEntry::blockType, aaruformat_context::cached_ddt_offset, aaruformat_context::cached_ddt_position, aaruformat_context::cached_secondary_ddt2, DdtHeader2::cmpCrc64, DdtHeader2::cmpLength, DdtHeader2::compression, aaruformat_context::compression_enabled, DdtHeader2::crc64, DdtHeader2::dataShift, IndexEntry::dataType, DeDuplicationTableSecondary, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, DdtHeader2::length, DdtHeader2::levels, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, DdtHeader2::previousLevelOffset, aaruformat_context::primary_ddt_offset, DdtHeader2::start, DdtHeader2::tableLevel, DdtHeader2::tableShift, TRACE, DdtHeader2::type, aaruformat_context::user_data_ddt2, aaruformat_context::user_data_ddt_header, and UserData.

Referenced by aaruf_close().

## ◆ write_checksum_block()

void write_checksum_block | ( | aaruformat_context * | ctx | ) |
| static

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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Definition at line 654 of file close.c.

References AARU_FEATURE_RW_BLAKE3, aaruf_md5_final(), aaruf_sha1_final(), aaruf_sha256_final(), aaruf_spamsum_final(), aaruf_spamsum_free(), Blake3, Checksums::blake3, aaruformat_context::blake3_context, DdtHeader2::blockAlignmentShift, IndexEntry::blockType, aaruformat_context::calculating_blake3, aaruformat_context::calculating_md5, aaruformat_context::calculating_sha1, aaruformat_context::calculating_sha256, aaruformat_context::calculating_spamsum, ChecksumBlock, aaruformat_context::checksums, IndexEntry::dataType, ChecksumHeader::entries, AaruHeaderV2::featureCompatible, FUZZY_MAX_RESULT, Checksums::hasBlake3, Checksums::hasMd5, Checksums::hasSha1, Checksums::hasSha256, Checksums::hasSpamSum, aaruformat_context::header, ChecksumHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, ChecksumEntry::length, ChecksumHeader::length, Md5, Checksums::md5, aaruformat_context::md5_context, MD5_DIGEST_LENGTH, IndexEntry::offset, Sha1, Checksums::sha1, aaruformat_context::sha1_context, SHA1_DIGEST_LENGTH, Sha256, Checksums::sha256, aaruformat_context::sha256_context, SHA256_DIGEST_LENGTH, SpamSum, Checksums::spamsum, aaruformat_context::spamsum_context, TRACE, ChecksumEntry::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_cicm_block()

void write_cicm_block | ( | const aaruformat_context * | ctx | ) |
| static

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)

Parameters

  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
  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
  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
  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.
  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 also
  CicmMetadataBlock for the on-disk structure definition.
  aaruf_get_cicm_metadata() for retrieving CICM XML from opened images.

Definition at line 3677 of file close.c.

References DdtHeader2::blockAlignmentShift, IndexEntry::blockType, aaruformat_context::cicm_block, aaruformat_context::cicm_block_header, CicmBlock, IndexEntry::dataType, CicmMetadataBlock::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, CicmMetadataBlock::length, IndexEntry::offset, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_dumphw_block()

void write_dumphw_block | ( | aaruformat_context * | ctx | ) |
| static

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)

Parameters

  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
  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
  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")
  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
  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)
  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
  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.
  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 also
  DumpHardwareHeader for the block header structure definition.
  DumpHardwareEntry for the per-environment entry structure definition.
  DumpExtent for the extent range structure definition.
  aaruf_get_dumphw() for retrieving dump hardware from opened images.
  process_dumphw_block() for the loading process during image opening.

Definition at line 3447 of file close.c.

References aaruf_crc64_data(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, DumpHardwareHeader::crc64, IndexEntry::dataType, aaruformat_context::dump_hardware_entries_with_data, aaruformat_context::dump_hardware_header, DumpHardwareBlock, DumpHardwareHeader::entries, DumpHardwareEntriesWithData::entry, DumpHardwareEntriesWithData::extents, DumpHardwareEntry::extents, FATAL, DumpHardwareEntriesWithData::firmware, DumpHardwareEntry::firmwareLength, DumpHardwareHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, DumpHardwareHeader::length, DumpHardwareEntriesWithData::manufacturer, DumpHardwareEntry::manufacturerLength, DumpHardwareEntriesWithData::model, DumpHardwareEntry::modelLength, IndexEntry::offset, DumpHardwareEntriesWithData::revision, DumpHardwareEntry::revisionLength, DumpHardwareEntriesWithData::serial, DumpHardwareEntry::serialLength, DumpHardwareEntriesWithData::softwareName, DumpHardwareEntry::softwareNameLength, DumpHardwareEntriesWithData::softwareOperatingSystem, DumpHardwareEntry::softwareOperatingSystemLength, DumpHardwareEntriesWithData::softwareVersion, DumpHardwareEntry::softwareVersionLength, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_dvd_long_sector_blocks()

void write_dvd_long_sector_blocks | ( | aaruformat_context * | ctx | ) |

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: If any of the four auxiliary buffers is NULL, the function returns immediately without writing anything. This is an all-or-nothing operation - either all four blocks are written or none.

Parameters

  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
  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
  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
  Memory Management:
  - The function allocates temporary buffers for compression when enabled
  - Auxiliary data buffers are managed by the caller
  - Compression buffers are freed after each block is written
  - Source data memory is freed later during context cleanup (aaruf_close)
  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
  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.
  Compression is applied if enabled. The blocks may be stored compressed or uncompressed depending on the compression_enabled setting and compression effectiveness.
  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 also
  aaruf_write_sector_long() for writing individual DVD long sectors that populate these buffers.
  BlockHeader for the block header structure definition.

Definition at line 1810 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, DvdSectorCprMai, DvdSectorEdc, DvdSectorId, DvdSectorIed, BlockHeader::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, aaruformat_context::sector_cpr_mai, aaruformat_context::sector_edc, aaruformat_context::sector_id, aaruformat_context::sector_ied, ImageInfo::Sectors, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_dvd_title_key_decrypted_block()

void write_dvd_title_key_decrypted_block | ( | const aaruformat_context * | ctx | ) |
| static

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: Applied if enabled (LZMA compression)
- CRC64: Computed over the 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 = ctx->compression_enabled ? Lzma : None
   - length = (negative + sectors + overflow) × 5
   - cmpLength = compressed size or original size if compression not effective
   - crc64 = CRC64 of original key buffer
   - cmpCrc64 = CRC64 of compressed data or same as crc64 if uncompressed
5. Write BlockHeader (sizeof(BlockHeader) bytes)
6. Write LZMA properties if compressed (LZMA_PROPERTIES_LENGTH bytes)
7. Write decrypted title key data buffer (compressed or uncompressed)
8. 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)

Parameters

  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.
  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.
  Decrypted title keys are sensitive cryptographic material. Applications should consider access control and security implications when storing and distributing images containing decrypted keys.
  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.
  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.
  Do not call this function directly. It is invoked automatically by aaruf_close() as part of the image finalization sequence.

Definition at line 2247 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, DvdSectorTitleKeyDecrypted, BlockHeader::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, aaruformat_context::sector_decrypted_title_key, ImageInfo::Sectors, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_geometry_block()

void write_geometry_block | ( | const aaruformat_context * | ctx | ) |
| static

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).

Parameters

  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.

Definition at line 3028 of file close.c.

References DdtHeader2::blockAlignmentShift, IndexEntry::blockType, IndexEntry::dataType, aaruformat_context::geometry_block, GeometryBlock, GeometryBlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, IndexEntry::offset, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_index_block()

int32_t write_index_block | ( | aaruformat_context * | ctx | ) |
| static

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).

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Returns
  AARUF_STATUS_OK on success; AARUF_ERROR_CANNOT_WRITE_HEADER if the index header, any entry, or the header back-patch fails.

Return values

  AARUF_STATUS_OK | Index written and header updated.
  AARUF_ERROR_CANNOT_WRITE_HEADER | Failed writing index header, entries, or updating main header.

Definition at line 3864 of file close.c.

References aaruf_crc64_final(), aaruf_crc64_init(), aaruf_crc64_update(), AARUF_ERROR_CANNOT_WRITE_HEADER, AARUF_STATUS_OK, DdtHeader2::blockAlignmentShift, IndexEntry::blockType, IndexHeader3::crc64, IndexEntry::dataType, IndexHeader3::entries, aaruformat_context::header, IndexHeader3::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, IndexBlock3, AaruHeaderV2::indexOffset, IndexEntry::offset, IndexHeader3::previous, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_media_tags()

void write_media_tags | ( | const aaruformat_context * | ctx | ) |
| static

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()
- Compressed if enabled (compression = ctx->compression_enabled ? Lzma : None)
- CRC64-protected: the checksum is computed over the raw tag data and stored in crc64; cmpCrc64 stores the checksum of compressed data or equals crc64 if uncompressed
- Followed immediately by the tag's data payload (compressed or uncompressed)

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, compression setting, length, CRC64
4. Compress tag data if enabled and compression is effective
5. Write BlockHeader (sizeof(BlockHeader) bytes)
6. Write LZMA properties if compressed (LZMA_PROPERTIES_LENGTH bytes)
7. Write tag data (compressed or uncompressed)
8. 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.

Parameters

  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 also
  aaruf_write_media_tag() for adding tags to the context during image creation.
  aaruf_get_datatype_for_media_tag_type() for type conversion logic.
  mediaTagEntry for the hash table entry structure.

Definition at line 2411 of file close.c.

References aaruf_crc64_data(), aaruf_get_datatype_for_media_tag_type(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, mediaTagEntry::data, DataBlock, IndexEntry::dataType, BlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, mediaTagEntry::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, aaruformat_context::mediaTags, None, IndexEntry::offset, TRACE, BlockHeader::type, mediaTagEntry::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_metadata_block()

void write_metadata_block | ( | aaruformat_context * | ctx | ) |
| static

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

Parameters

  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
  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.
  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 also
  MetadataBlockHeader for the on-disk structure definition.
  aaruf_set_creator() for populating the creator field.
  aaruf_set_comments() for populating the comments field.
  aaruf_set_media_title() for populating the media title field.

Definition at line 3164 of file close.c.

References DdtHeader2::blockAlignmentShift, MetadataBlockHeader::blockSize, IndexEntry::blockType, aaruformat_context::comments, MetadataBlockHeader::commentsLength, MetadataBlockHeader::commentsOffset, aaruformat_context::creator, MetadataBlockHeader::creatorLength, MetadataBlockHeader::creatorOffset, IndexEntry::dataType, aaruformat_context::drive_firmware_revision, aaruformat_context::drive_manufacturer, aaruformat_context::drive_model, aaruformat_context::drive_serial_number, MetadataBlockHeader::driveFirmwareRevisionLength, MetadataBlockHeader::driveFirmwareRevisionOffset, MetadataBlockHeader::driveManufacturerLength, MetadataBlockHeader::driveManufacturerOffset, MetadataBlockHeader::driveModelLength, MetadataBlockHeader::driveModelOffset, MetadataBlockHeader::driveSerialNumberLength, MetadataBlockHeader::driveSerialNumberOffset, MetadataBlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, MetadataBlockHeader::lastMediaSequence, aaruformat_context::media_barcode, aaruformat_context::media_manufacturer, aaruformat_context::media_model, aaruformat_context::media_part_number, aaruformat_context::media_serial_number, aaruformat_context::media_title, MetadataBlockHeader::mediaBarcodeLength, MetadataBlockHeader::mediaBarcodeOffset, MetadataBlockHeader::mediaManufacturerLength, MetadataBlockHeader::mediaManufacturerOffset, MetadataBlockHeader::mediaModelLength, MetadataBlockHeader::mediaModelOffset, MetadataBlockHeader::mediaPartNumberLength, MetadataBlockHeader::mediaPartNumberOffset, MetadataBlockHeader::mediaSequence, MetadataBlockHeader::mediaSerialNumberLength, MetadataBlockHeader::mediaSerialNumberOffset, MetadataBlockHeader::mediaTitleLength, MetadataBlockHeader::mediaTitleOffset, aaruformat_context::metadata_block_header, MetadataBlock, IndexEntry::offset, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_mode2_subheaders_block()

void write_mode2_subheaders_block | ( | aaruformat_context * | ctx | ) |
| static

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 (compression enabled if configured) and adds an IndexEntry.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode; ctx->mode2_subheaders must point to a buffer sized for the described sector span.

Definition at line 850 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, BlockHeader::cmpCrc64, BlockHeader::cmpLength, CompactDiscMode2Subheader, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, BlockHeader::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, aaruformat_context::mode2_subheaders, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, ImageInfo::Sectors, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_primary_ddt()

int32_t write_primary_ddt | ( | aaruformat_context * | ctx | ) |
| static

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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Returns
  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).

Return values

  AARUF_STATUS_OK | Success or nothing to do (no multi-level primary table present).
  AARUF_ERROR_CANNOT_WRITE_HEADER | Failed writing header or primary table data.

Definition at line 283 of file close.c.

References aaruf_crc64_final(), aaruf_crc64_init(), aaruf_crc64_update(), AARUF_ERROR_CANNOT_WRITE_HEADER, AARUF_STATUS_OK, IndexEntry::blockType, DdtHeader2::cmpCrc64, DdtHeader2::cmpLength, DdtHeader2::compression, DdtHeader2::crc64, IndexEntry::dataType, DeDuplicationTable2, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, DdtHeader2::length, None, IndexEntry::offset, aaruformat_context::primary_ddt_offset, DdtHeader2::tableShift, TRACE, DdtHeader2::type, aaruformat_context::user_data_ddt2, aaruformat_context::user_data_ddt_header, and UserData.

Referenced by aaruf_close().

## ◆ write_sector_prefix()

void write_sector_prefix | ( | aaruformat_context * | ctx | ) |
| static

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, compressed if enabled, 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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Definition at line 966 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, CdSectorPrefix, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, BlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, None, IndexEntry::offset, aaruformat_context::sector_prefix, aaruformat_context::sector_prefix_offset, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_sector_prefix_ddt()

void write_sector_prefix_ddt | ( | aaruformat_context * | ctx | ) |
| static

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: Bits 63..61 : SectorStatus enum value (see enums.h, values already positioned for v2 layout). Bits 60..0 : Index 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).
- The table length equals (negative + Sectors + overflow) * entrySize.
- dataShift is set to 4 (2^4 = 16) expressing the granularity of referenced prefix units.
- Compression is applied if enabled; 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.

Parameters

  ctx | Pointer to a valid aaruformatContext in write mode (must not be NULL).

Definition at line 1206 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, DdtHeader2::blocks, IndexEntry::blockType, CdSectorPrefix, DdtHeader2::cmpCrc64, DdtHeader2::cmpLength, DdtHeader2::compression, aaruformat_context::compression_enabled, DdtHeader2::crc64, DdtHeader2::dataShift, IndexEntry::dataType, DeDuplicationTable2, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, DdtHeader2::length, DdtHeader2::levels, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, aaruformat_context::sector_prefix_ddt2, ImageInfo::Sectors, DdtHeader2::start, DdtHeader2::tableLevel, DdtHeader2::tableShift, TRACE, DdtHeader2::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_sector_subchannel()

void write_sector_subchannel | ( | const aaruformat_context * | ctx | ) |
| static

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). Compression is conditionally applied based on media type and compression settings:

- Optical Disc: When compression is enabled, applies Claunia Subchannel Transform (CST) followed by LZMA compression (LzmaClauniaSubchannelTransform). If the compressed size is not smaller than the original, falls back to uncompressed storage.
- Block Media: Always attempts LZMA compression for tag data. If the compressed size is not smaller than the original, falls back to uncompressed storage. The data is written 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.

Parameters

  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.

Definition at line 1508 of file close.c.

References aaruf_crc64_data(), aaruf_cst_transform(), aaruf_lzma_encode_buffer(), AppleFileWare, AppleProfile, AppleProfileTag, AppleSonyDS, AppleSonySS, AppleSonyTag, DdtHeader2::blockAlignmentShift, BlockMedia, IndexEntry::blockType, CdSectorSubchannel, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, BlockHeader::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, LzmaClauniaSubchannelTransform, ImageInfo::MediaType, ImageInfo::MetadataMediaType, DdtHeader2::negative, None, IndexEntry::offset, OpticalDisc, DdtHeader2::overflow, PriamDataTower, PriamDataTowerTag, aaruformat_context::sector_subchannel, ImageInfo::Sectors, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_sector_suffix()

void write_sector_suffix | ( | aaruformat_context * | ctx | ) |
| static

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).
- Compression is applied if enabled; 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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode. Must not be NULL.

Definition at line 1088 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, CdSectorSuffix, BlockHeader::cmpCrc64, BlockHeader::cmpLength, BlockHeader::compression, aaruformat_context::compression_enabled, BlockHeader::crc64, DataBlock, IndexEntry::dataType, BlockHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, BlockHeader::length, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, None, IndexEntry::offset, aaruformat_context::sector_suffix, aaruformat_context::sector_suffix_offset, TRACE, BlockHeader::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_sector_suffix_ddt()

void write_sector_suffix_ddt | ( | aaruformat_context * | ctx | ) |
| static

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 (DDT v2 semantics): Bits 63..61 : SectorStatus enumeration (already aligned for direct storage; no legacy masks used). Bits 60..0 : Index 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.
- 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).
- Compression is applied if enabled; CRC64 protects the table bytes.
- Alignment: The table is aligned to 2^(blockAlignmentShift) before writing to guarantee block boundary access.
- Idempotence: If sectorSuffixDdt2 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->sectorSuffixDdt2 must point to a fully populated contiguous array of uint16_t entries.

Parameters

  ctx | Active aaruformatContext being finalized.

Definition at line 1350 of file close.c.

References aaruf_crc64_data(), aaruf_lzma_encode_buffer(), DdtHeader2::blockAlignmentShift, DdtHeader2::blocks, IndexEntry::blockType, CdSectorSuffix, DdtHeader2::cmpCrc64, DdtHeader2::cmpLength, DdtHeader2::compression, aaruformat_context::compression_enabled, DdtHeader2::crc64, DdtHeader2::dataShift, IndexEntry::dataType, DeDuplicationTable2, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::image_info, aaruformat_context::imageStream, aaruformat_context::index_entries, DdtHeader2::length, DdtHeader2::levels, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, DdtHeader2::negative, None, IndexEntry::offset, DdtHeader2::overflow, aaruformat_context::sector_suffix_ddt2, ImageInfo::Sectors, DdtHeader2::start, DdtHeader2::tableLevel, DdtHeader2::tableShift, TRACE, DdtHeader2::type, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_single_level_ddt()

int32_t write_single_level_ddt | ( | aaruformat_context * | ctx | ) |
| static

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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode with tableShift == 0.

Returns
  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.

Return values

  AARUF_STATUS_OK | Success or nothing to do (not single-level / buffers missing).
  AARUF_ERROR_CANNOT_WRITE_HEADER | Failed writing header or table data.

Definition at line 369 of file close.c.

References aaruf_crc64_data(), AARUF_ERROR_CANNOT_WRITE_HEADER, AARUF_ERROR_NOT_ENOUGH_MEMORY, aaruf_lzma_encode_buffer(), AARUF_STATUS_OK, DdtHeader2::blockAlignmentShift, IndexEntry::blockType, DdtHeader2::cmpCrc64, DdtHeader2::cmpLength, DdtHeader2::compression, aaruformat_context::compression_enabled, DdtHeader2::crc64, IndexEntry::dataType, DeDuplicationTable2, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, DdtHeader2::length, DdtHeader2::levels, Lzma, aaruformat_context::lzma_dict_size, LZMA_PROPERTIES_LENGTH, None, IndexEntry::offset, DdtHeader2::previousLevelOffset, DdtHeader2::tableLevel, DdtHeader2::tableShift, TRACE, DdtHeader2::type, aaruformat_context::user_data_ddt2, aaruformat_context::user_data_ddt_header, and UserData.

Referenced by aaruf_close(), and write_tape_ddt().

## ◆ write_tape_ddt()

int32_t write_tape_ddt | ( | aaruformat_context * | ctx | ) |
| static

Converts tape DDT hash table to array format and writes it as a single-level DDT.

This function is specifically designed for tape media images where sectors have been tracked using a sparse hash table (UTHASH) during write operations. It converts the hash-based tape DDT into a traditional array-based DDT structure suitable for serialization to disk. The function performs a complete transformation from the sparse hash representation to a dense array representation, then delegates the actual write operation to write_single_level_ddt().

The conversion process involves:

1. Validating the context is for tape media
2. Scanning the hash table to determine the maximum sector address (key)
3. Allocating a contiguous array large enough to hold all entries up to max_key
4. Populating the array by copying hash table entries to their corresponding indices
5. Initializing a DDT v2 header with appropriate metadata
6. Calling write_single_level_ddt() to serialize the DDT to disk

Hash Table to Array Conversion: The tape DDT hash table uses sector addresses as keys and DDT entries as values. This function creates a zero-initialized array of size (max_key + 1) and copies each hash entry to array[entry->key] = entry->value. Sectors not present in the hash table remain as zero entries in the array, which indicates SectorStatusNotDumped in the DDT format.

Memory Allocation: The function always uses BigDdtSizeType (32-bit entries) for tape DDTs, allocating (max_key + 1) * sizeof(uint32_t) bytes. This provides sufficient capacity for the 28-bit data + 4-bit status encoding used in tape DDT entries.

DDT Header Configuration: The userDataDdtHeader is configured for a single-level DDT v2 structure:

- identifier: DeDuplicationTable2
- type: UserData
- compression: Determined by ctx->compression_enabled (Lzma or None)
- levels: 1 (single-level structure)
- tableLevel: 0 (top-level table)
- tableShift: 0 (no multi-level indirection)
- sizeType: BigDdtSizeType (32-bit entries)
- entries/blocks: max_key + 1
- negative/overflow: 0 (not used for tape)

Parameters

  ctx | Pointer to the aaruformat context. Must not be NULL and must be in write mode. The context must have is_tape set to true and tapeDdt hash table populated. The ctx->userDataDdtBig array will be allocated and populated by this function. The ctx->userDataDdtHeader will be initialized with DDT metadata.

Returns
  Returns one of the following status codes:

Return values

  AARUF_STATUS_OK | (0) Successfully converted and wrote the tape DDT. This occurs when: The context is valid and is_tape is true Memory allocation for the DDT array succeeds The hash table entries are successfully copied to the array write_single_level_ddt() completes successfully The DDT is written to disk and indexed
  AARUF_STATUS_INVALID_CONTEXT | (-2) The context is not for tape media. This occurs when: ctx->is_tape is false This function was called for a disk/optical image instead of tape
  AARUF_ERROR_NOT_ENOUGH_MEMORY | (-9) Memory allocation failed. This occurs when: calloc() fails to allocate the userDataDdtBig array Insufficient system memory for (max_key + 1) * 4 bytes
  AARUF_ERROR_CANNOT_WRITE_HEADER | (-21) Writing the DDT failed. This can occur when: write_single_level_ddt() fails to write the DDT header File I/O errors prevent writing the DDT data Disk full or other storage errors This error is propagated from write_single_level_ddt()

Note
  This function is only called during image finalization (aaruf_close) for tape images. It should not be called for disk or optical media images.
  Hash Table Iteration:
  - Uses HASH_ITER macro from UTHASH to safely traverse all entries
  - Finds maximum key in first pass to determine array size
  - Copies entries in second pass to populate the array
  - Empty (zero) array slots represent sectors not written to tape
  Memory Ownership:
  - Allocates ctx->userDataDdtBig which becomes owned by the context
  - The allocated array is freed during context cleanup (not in this function)
  - The original hash table (ctx->tapeDdt) is freed separately during cleanup
  Single-Level DDT Choice:
  - Tape DDTs always use single-level structure (tableShift = 0)
  - Multi-level DDTs are not used because tape access patterns are typically sparse
  - The hash table already provides efficient sparse storage during write
  - Conversion to dense array only happens once at close time
  Compression:
  - The actual compression is handled by write_single_level_ddt()
  - Compression type is determined by ctx->compression_enabled flag
  - If enabled, LZMA compression is applied to the DDT array
  - Compression may be disabled if it doesn't reduce size

Warning
  The function assumes tapeDdt hash table is properly populated. An empty hash table will result in a DDT with a single zero entry (max_key = 0, entries = 1).
  This function modifies ctx->userDataDdtHeader and ctx->userDataDdtBig. These must not be modified by other code during the close operation.
  The allocated array size is (max_key + 1), which could be very large if tape sectors have high addresses with sparse distribution. Memory usage should be considered.

See also
  set_ddt_tape() for how entries are added to the hash table during write operations
  write_single_level_ddt() for the actual DDT serialization logic
  TapeDdtHashEntry for the hash table entry structure

Definition at line 596 of file close.c.

References AARUF_ERROR_NOT_ENOUGH_MEMORY, AARUF_STATUS_INVALID_CONTEXT, DdtHeader2::blocks, DdtHeader2::cmpLength, DdtHeader2::compression, aaruformat_context::compression_enabled, DeDuplicationTable2, DdtHeader2::entries, DdtHeader2::identifier, aaruformat_context::is_tape, TapeDdtHashEntry::key, DdtHeader2::length, DdtHeader2::levels, Lzma, DdtHeader2::negative, None, DdtHeader2::overflow, DdtHeader2::previousLevelOffset, DdtHeader2::start, DdtHeader2::tableLevel, DdtHeader2::tableShift, aaruformat_context::tape_ddt, TRACE, DdtHeader2::type, aaruformat_context::user_data_ddt2, aaruformat_context::user_data_ddt_header, UserData, TapeDdtHashEntry::value, and write_single_level_ddt().

Referenced by aaruf_close().

## ◆ write_tape_file_block()

void write_tape_file_block | ( | const aaruformat_context * | ctx | ) |
| static

Serialize the tape file metadata block to the image file.

This function writes a TapeFileBlock containing the complete tape file structure metadata to the Aaru image file. The tape file block documents all logical files present on the tape medium, recording each file's partition number, file number, and block range (first and last block addresses). This metadata enables random access to specific files within the tape image and preserves the original tape's logical organization for archival purposes.

The tape file block is optional; if no tape file metadata has been populated (ctx->tapeFiles hash table is NULL or empty), the function returns immediately without writing anything. This no-op behavior allows the close operation to proceed gracefully whether or not tape file structure metadata was included during image creation.

Block Structure: The serialized block consists of:

+-------------------------+
| TapeFileHeader (24 B) | <- identifier, entries, length, crc64
+-------------------------+
| TapeFileEntry 0 (21 B) | <- File, Partition, FirstBlock, LastBlock
| TapeFileEntry 1 (21 B) |
| ... |
| TapeFileEntry (n-1) |
+-------------------------+
TapeFileEntry
Describes a single logical file on a tape medium.
Definition tape.h:134
TapeFileHeader
struct TapeFileHeader TapeFileHeader
TapeFileEntry
struct TapeFileEntry TapeFileEntry

Processing Flow:

1. Entry Enumeration: Iterate through ctx->tapeFiles hash table to count entries
2. Buffer Allocation: Allocate temporary buffer for all TapeFileEntry structures
3. Data Copying: Copy each file entry from hash table to buffer sequentially
4. Header Construction: Build TapeFileHeader with entry count and CRC64 checksum
5. Alignment: Seek to EOF and align to block boundary (blockAlignmentShift)
6. Write Operations: Write header followed by entry array to image stream
7. Indexing: Add IndexEntry pointing to this block for fast location during reads
8. Cleanup: Free temporary buffer

Hash Table Iteration: The function uses UTHASH's HASH_ITER macro to safely traverse ctx->tapeFiles:

- First pass: Count total entries in the hash table
- Second pass: Copy each TapeFileEntry to the output buffer
- The iteration order depends on hash table internals, not insertion order
- For deterministic output, entries could be sorted before writing (not currently done)

CRC64 Integrity Protection: A CRC64-ECMA checksum is computed over the complete array of TapeFileEntry structures using aaruf_crc64_data(). This checksum is stored in the TapeFileHeader and verified during image opening by process_tape_files_block() to detect corruption in the file table. The checksum covers only the entry data, not the header itself.

Alignment Strategy: Before writing, the file position is:

1. Moved to EOF using fseek(SEEK_END)
2. Aligned forward to next boundary: (position + alignment_mask) & ~alignment_mask
3. Where alignment_mask = (1 << blockAlignmentShift) - 1 This ensures the tape file block starts 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 TapeFileHeader (sizeof(TapeFileHeader) = 24 bytes)
2. Write TapeFileEntry array (tape_file_block.length bytes)

Both writes must succeed for the index entry to be added. If either write fails, the block is incomplete but the function continues (no error propagation).

Indexing: On successful write, an IndexEntry is created and pushed to ctx->indexEntries:

- blockType = TapeFileBlock (identifies this as tape file metadata)
- dataType = 0 (tape file blocks have no subtype)
- offset = file position where TapeFileHeader was written

This index entry enables process_tape_files_block() to quickly locate the tape file metadata during subsequent image opens without scanning the entire file.

Entry Order: The current implementation writes entries in hash table iteration order, which is non-deterministic and depends on the hash function and insertion sequence. For better compatibility and reproducibility, entries should ideally be sorted by:

1. Partition number (ascending)
2. File number within partition (ascending) However, the current implementation does not enforce this ordering.

Error Handling: The function handles errors gracefully without propagating them:

- NULL hash table: Return immediately (no tape files to write)
- Memory allocation failure: Log via TRACE and return (block not written)
- Write failures: Silent (index entry not added, block incomplete)

This opportunistic approach ensures that tape file metadata write failures do not prevent the image from being created, though the resulting image will lack file structure metadata.

Memory Management:

- Allocates temporary buffer sized to hold all TapeFileEntry structures
- Buffer is zero-initialized with memset for consistent padding bytes
- Buffer is always freed before the function returns, even on write failure
- Source data in ctx->tapeFiles is not modified and is freed later during cleanup

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).

Use Cases:

- Preserving tape file structure for archival and forensic purposes
- Enabling random access to specific files within tape images
- Documenting multi-file tape organization for analysis tools
- Supporting tape formats with complex file/partition layouts
- Facilitating tape image validation and structure verification

Relationship to Other Functions:

- File entries are added via aaruf_set_tape_file() during image creation
- Entries are stored in ctx->tapeFiles hash table until image close
- This function serializes the hash table to disk during aaruf_close()
- process_tape_files_block() reads and reconstructs the hash table during aaruf_open()

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode. Must not be NULL. The tapeFiles hash table should be populated if tape file metadata exists. The imageStream must be open and writable. The indexEntries array must be initialized for adding the index entry.

Note
  The tape file block is written near the end of the image file, after sector data and before the final index block. The exact position depends on what other metadata blocks are present in the image.
  If ctx->tapeFiles is NULL or empty, the function returns immediately without writing anything. This is not an error condition - it simply means the image contains no tape file structure metadata.
  Memory allocation failure during buffer creation results in no tape file block being written, but does not prevent the image from being successfully created. The image will simply lack tape file metadata.
  The TapeFileHeader.entries field is intentionally set to tape_file_count, not stored separately. The count is derived dynamically from the hash table size.
  Entry ordering is not guaranteed to match insertion order or logical order. Reading applications should sort entries by partition/file number if ordered access is required.

Warning
  Write failures (fwrite returns != 1) are silently ignored. The function does not return an error code or set errno. Partial writes may leave the block incomplete, which will be detected during subsequent reads via CRC mismatch.
  The temporary buffer allocation may fail on systems with limited memory or when the tape has an extremely large number of files. Allocation failures result in silent no-op; the image is created without tape file metadata.
  Bounds checking during iteration protects against buffer overruns. If index exceeds tape_file_count (which should never occur), the loop breaks early as a sanity check.

See also
  TapeFileHeader for the block header structure definition
  TapeFileEntry for individual file entry structure definition
  tapeFileHashEntry for the hash table entry structure
  aaruf_set_tape_file() for adding tape files during image creation
  process_tape_files_block() for the loading process during image opening
  aaruf_get_tape_file() for retrieving tape file information from opened images

Definition at line 2671 of file close.c.

References aaruf_crc64_data(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, TapeFileHeader::crc64, IndexEntry::dataType, TapeFileHashEntry::fileEntry, TapeFileHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, TapeFileHeader::length, IndexEntry::offset, aaruformat_context::tape_files, TapeFileBlock, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_tape_partition_block()

void write_tape_partition_block | ( | const aaruformat_context * | ctx | ) |
| static

Serialize the tape partition metadata block to the image file.

This function writes a TapePartitionBlock containing the complete tape partition structure metadata to the Aaru image file. The tape partition block documents all physical partitions present on the tape medium, recording each partition's partition number and block range (first and last block addresses). This metadata enables proper interpretation of tape file locations, validation of partition boundaries, and preservation of the original tape's physical organization for archival purposes.

The tape partition block is optional; if no tape partition metadata has been populated (ctx->tapePartitions hash table is NULL or empty), the function returns immediately without writing anything. This no-op behavior allows the close operation to proceed gracefully whether or not tape partition structure metadata was included during image creation.

Block Structure: The serialized block consists of:

+-----------------------------+
| TapePartitionHeader (24 B) | <- identifier, entries, length, crc64
+-----------------------------+
| TapePartitionEntry 0 (17 B) | <- Number, FirstBlock, LastBlock
| TapePartitionEntry 1 (17 B) |
| ... |
| TapePartitionEntry (n-1) |
+-----------------------------+
TapePartitionEntry
Describes a single physical partition on a tape medium.
Definition tape.h:320
TapePartitionHeader
struct TapePartitionHeader TapePartitionHeader
TapePartitionEntry
struct TapePartitionEntry TapePartitionEntry

Processing Flow:

1. Entry Enumeration: Iterate through ctx->tapePartitions hash table to count entries
2. Buffer Allocation: Allocate temporary buffer for all TapePartitionEntry structures
3. Data Copying: Copy each partition entry from hash table to buffer sequentially
4. Header Construction: Build TapePartitionHeader with entry count and CRC64 checksum
5. Alignment: Seek to EOF and align to block boundary (blockAlignmentShift)
6. Write Operations: Write header followed by entry array to image stream
7. Indexing: Add IndexEntry pointing to this block for fast location during reads
8. Cleanup: Free temporary buffer

Hash Table Iteration: The function uses UTHASH's HASH_ITER macro to safely traverse ctx->tapePartitions:

- First pass: Count total entries in the hash table
- Second pass: Copy each TapePartitionEntry to the output buffer
- The iteration order depends on hash table internals, not insertion order
- For deterministic output, entries could be sorted by partition number (not currently done)

CRC64 Integrity Protection: A CRC64-ECMA checksum is computed over the complete array of TapePartitionEntry structures using aaruf_crc64_data(). This checksum is stored in the TapePartitionHeader and verified during image opening by process_tape_partitions_block() to detect corruption in the partition table. The checksum covers only the entry data, not the header itself.

Alignment Strategy: Before writing, the file position is:

1. Moved to EOF using fseek(SEEK_END)
2. Aligned forward to next boundary: (position + alignment_mask) & ~alignment_mask
3. Where alignment_mask = (1 << blockAlignmentShift) - 1 This ensures the tape partition block starts 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 TapePartitionHeader (sizeof(TapePartitionHeader) = 24 bytes)
2. Write TapePartitionEntry array (tape_partition_block.length bytes)

Both writes must succeed for the index entry to be added. If either write fails, the block is incomplete but the function continues (no error propagation).

Indexing: On successful write, an IndexEntry is created and pushed to ctx->indexEntries:

- blockType = TapePartitionBlock (identifies this as tape partition metadata)
- dataType = 0 (tape partition blocks have no subtype)
- offset = file position where TapePartitionHeader was written

This index entry enables process_tape_partitions_block() to quickly locate the tape partition metadata during subsequent image opens without scanning the entire file.

Entry Order: The current implementation writes entries in hash table iteration order, which is non-deterministic and depends on the hash function and insertion sequence. For better compatibility and reproducibility, entries should ideally be sorted by partition number (ascending). However, the current implementation does not enforce this ordering.

Partition Block Ranges: Each partition entry defines an independent block address space:

- FirstBlock: Starting block address (often 0, but format-dependent)
- LastBlock: Ending block address (inclusive)
- Block count: (LastBlock - FirstBlock + 1)

Different partitions may have overlapping logical block numbers (e.g., partition 0 and partition 1 can both have blocks 0-1000). The partition metadata is essential for correctly interpreting tape file locations, as files reference partition numbers in their definitions.

Error Handling: The function handles errors gracefully without propagating them:

- NULL hash table: Return immediately (no tape partitions to write)
- Memory allocation failure: Log via TRACE and return (block not written)
- Write failures: Silent (index entry not added, block incomplete)

This opportunistic approach ensures that tape partition metadata write failures do not prevent the image from being created, though the resulting image will lack partition structure metadata.

Memory Management:

- Allocates temporary buffer sized to hold all TapePartitionEntry structures
- Buffer is zero-initialized with memset for consistent padding bytes
- Buffer is always freed before the function returns, even on write failure
- Source data in ctx->tapePartitions is not modified and is freed later during cleanup

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).

Use Cases:

- Preserving tape partition structure for archival and forensic purposes
- Documenting multi-partition tape layouts (LTO, DLT, AIT formats)
- Enabling validation of file block ranges against partition boundaries
- Supporting tape formats with complex partition organizations
- Facilitating tape image validation and structure verification

Relationship to Other Functions:

- Partition entries are added via aaruf_set_tape_partition() during image creation
- Entries are stored in ctx->tapePartitions hash table until image close
- This function serializes the hash table to disk during aaruf_close()
- process_tape_partitions_block() reads and reconstructs the hash table during aaruf_open()
- Tape files (written by write_tape_file_block) reference these partitions

Format Considerations:

- Single-partition tapes: May omit TapePartitionBlock entirely (partition 0 implied)
- Multi-partition tapes: Should include complete partition layout for proper file access
- Block addresses are local to each partition in most tape formats
- Partition metadata is primarily informational and used for validation

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode. Must not be NULL. The tapePartitions hash table should be populated if tape partition metadata exists. The imageStream must be open and writable. The indexEntries array must be initialized for adding the index entry.

Note
  The tape partition block is written near the end of the image file, after sector data and before the final index block. The exact position depends on what other metadata blocks are present in the image.
  If ctx->tapePartitions is NULL or empty, the function returns immediately without writing anything. This is not an error condition - it simply means the image contains no tape partition structure metadata (typical for single-partition tapes).
  Memory allocation failure during buffer creation results in no tape partition block being written, but does not prevent the image from being successfully created. The image will simply lack tape partition metadata.
  The partition metadata should be consistent with file metadata. Files should only reference partitions that have been defined, and their block ranges should fall within the partition boundaries, though no automatic validation is performed.

Warning
  Write failures are not propagated as errors. If the write operation fails, the index entry is not added, but aaruf_close() continues with other finalization tasks. The resulting image may be incomplete or missing partition metadata.

See also
  write_tape_file_block() for writing tape file metadata (which references partitions)
  process_tape_partitions_block() for reading partition metadata during image open
  aaruf_set_tape_partition() for adding partition entries during image creation
  TapePartitionHeader for the block header structure
  TapePartitionEntry for individual partition entry structure

Definition at line 2903 of file close.c.

References aaruf_crc64_data(), DdtHeader2::blockAlignmentShift, IndexEntry::blockType, TapePartitionHeader::crc64, IndexEntry::dataType, TapePartitionHeader::identifier, aaruformat_context::imageStream, aaruformat_context::index_entries, TapePartitionHeader::length, IndexEntry::offset, TapePartitionHashEntry::partitionEntry, aaruformat_context::tape_partitions, TapePartitionBlock, TRACE, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().

## ◆ write_tracks_block()

void write_tracks_block | ( | aaruformat_context * | ctx | ) |
| static

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.

Parameters

  ctx | Pointer to an initialized aaruformatContext in write mode.

Definition at line 798 of file close.c.

References DdtHeader2::blockAlignmentShift, IndexEntry::blockType, IndexEntry::dataType, TracksHeader::entries, aaruformat_context::imageStream, aaruformat_context::index_entries, IndexEntry::offset, TRACE, aaruformat_context::track_entries, aaruformat_context::tracks_header, TracksBlock, and aaruformat_context::user_data_ddt_header.

Referenced by aaruf_close().