diff --git a/include/aaruformat/context.h b/include/aaruformat/context.h
index 08256bc..9affa9a 100644
--- a/include/aaruformat/context.h
+++ b/include/aaruformat/context.h
@@ -66,6 +66,9 @@ typedef struct FluxCaptureMapEntry FluxCaptureMapEntry;
* - If deduplicate == false, sectorHashMap may still be populated for bookkeeping but duplicates are stored
* independently.
* - If userDataDdtMini != NULL then userDataDdtBig == NULL (and vice versa) for a given level.
+ * - If no_user_data_ddt == true the image carries no user-data DDT (intentional, e.g. flux-only image): user_data_ddt,
+ * user_data_ddt2, cached_secondary_ddt2 and tape_ddt MUST all be NULL, and user_data_ddt_header.entries MUST be 0.
+ * Sector read/write APIs return AARUF_ERROR_USER_DATA_NOT_PRESENT in this state.
*/
#ifndef MD5_DIGEST_LENGTH
@@ -197,6 +200,7 @@ typedef struct aaruformat_context
int ddt_version; ///< DDT version in use (1=legacy, 2=v2 hierarchical).
uint8_t shift; ///< Legacy overall shift (deprecated by data_shift/table_shift).
bool in_memory_ddt; ///< True if primary (and possibly secondary) DDT loaded.
+ bool no_user_data_ddt; ///< True if image intentionally has no user-data DDT (e.g. flux-only image). When set, user_data_ddt*/tape_ddt MUST be NULL and user_data_ddt_header.entries MUST be 0.
/* Optical auxiliary buffers (NULL if not present) */
uint8_t *sector_prefix; ///< Raw per-sector prefix (e.g., sync+header) uncorrected.
diff --git a/include/aaruformat/errors.h b/include/aaruformat/errors.h
index cef2306..ccb6249 100644
--- a/include/aaruformat/errors.h
+++ b/include/aaruformat/errors.h
@@ -73,6 +73,7 @@
#define AARUF_ERROR_CANNOT_ENCRYPT_SECTOR (-34) ///< AES sector encryption failed.
#define AARUF_ERROR_CANNOT_DECRYPT_SECTOR (-35) ///< AES sector decryption failed.
#define AARUF_ERROR_MISSING_ENCRYPTION_KEY (-36) ///< Required encryption key not present in media tags.
+#define AARUF_ERROR_USER_DATA_NOT_PRESENT (-37) ///< Image has no user-data DDT (e.g. flux-only image); sector data is unavailable.
/** @} */
/** \name Non-fatal sector status codes (non-negative)
@@ -156,6 +157,8 @@ static inline const char *aaruformat_error_string(int code)
return "Cannot decrypt sector";
case AARUF_ERROR_MISSING_ENCRYPTION_KEY:
return "Missing encryption key";
+ case AARUF_ERROR_USER_DATA_NOT_PRESENT:
+ return "User data not present (flux-only image)";
/* Status */
case AARUF_STATUS_OK:
diff --git a/src/close_write.c b/src/close_write.c
index c5df5f4..c326f2a 100644
--- a/src/close_write.c
+++ b/src/close_write.c
@@ -69,6 +69,9 @@
*/
static int32_t write_cached_secondary_ddt(aaruformat_context *ctx)
{
+ // Flux-only images carry no DDT.
+ if(ctx->no_user_data_ddt) return AARUF_STATUS_OK;
+
// Write cached secondary table to file end and update primary table entry with its position
// Check if we have a cached table that needs to be written (either it has an offset or exists in memory)
bool has_cached_secondary_ddt =
@@ -296,6 +299,9 @@ static int32_t write_cached_secondary_ddt(aaruformat_context *ctx)
*/
static int32_t write_primary_ddt(aaruformat_context *ctx)
{
+ // Flux-only images carry no DDT.
+ if(ctx->no_user_data_ddt) return AARUF_STATUS_OK;
+
// Write the cached primary DDT table back to its position in the file
if(ctx->user_data_ddt_header.tableShift <= 0 || ctx->user_data_ddt2 == NULL) return AARUF_STATUS_OK;
@@ -398,6 +404,9 @@ static int32_t write_primary_ddt(aaruformat_context *ctx)
*/
static int32_t write_single_level_ddt(aaruformat_context *ctx)
{
+ // Flux-only images carry no DDT.
+ if(ctx->no_user_data_ddt) return AARUF_STATUS_OK;
+
// Write the single level DDT table block aligned just after the header
if(ctx->user_data_ddt_header.tableShift != 0 || ctx->user_data_ddt2 == NULL) return AARUF_STATUS_OK;
@@ -661,6 +670,9 @@ static int32_t write_tape_ddt(aaruformat_context *ctx)
{
if(!ctx->is_tape) return AARUF_STATUS_INVALID_CONTEXT;
+ // Flux-only images carry no DDT (defensive; tape images aren't flux-only today).
+ if(ctx->no_user_data_ddt) return AARUF_STATUS_OK;
+
// Traverse the tape DDT uthash and find the biggest key
uint64_t max_key = 0;
TapeDdtHashEntry *entry, *tmp;
diff --git a/src/create.c b/src/create.c
index 080023f..9675187 100644
--- a/src/create.c
+++ b/src/create.c
@@ -417,7 +417,28 @@ AARU_EXPORT void *AARU_CALL aaruf_create(const char *filepath, const uint32_t me
ctx->library_major_version = LIBAARUFORMAT_MAJOR_VERSION;
ctx->library_minor_version = LIBAARUFORMAT_MINOR_VERSION;
- if(!is_tape)
+ /* Flux-only images carry no sector/DDT data: the caller signals this by passing zero
+ * for all sector counts. In that case skip DDT initialization entirely and lay out the
+ * image like a tape (header followed by aligned blocks). Sector read/write APIs return
+ * AARUF_ERROR_USER_DATA_NOT_PRESENT for such contexts. */
+ const bool flux_only = !is_tape && user_sectors == 0 && negative_sectors == 0 && overflow_sectors == 0;
+
+ if(flux_only)
+ {
+ TRACE("Initializing flux-only image (no DDT)");
+ ctx->no_user_data_ddt = true;
+
+ /* Record alignment in both the container header and the (otherwise unused) DDT header
+ * so existing helpers that consult either get a consistent value. */
+ ctx->user_data_ddt_header.blockAlignmentShift = parsed_options.block_alignment;
+ ctx->header.blockAlignmentShift = parsed_options.block_alignment;
+ ctx->user_data_ddt_header.dataShift = parsed_options.data_shift;
+
+ const uint64_t alignment_mask = (1ULL << parsed_options.block_alignment) - 1;
+ ctx->next_block_position = sizeof(AaruHeaderV2);
+ ctx->next_block_position = ctx->next_block_position + alignment_mask & ~alignment_mask;
+ }
+ else if(!is_tape)
{ // Initialize DDT2
TRACE("Initializing DDT2");
ctx->in_memory_ddt = true;
@@ -541,7 +562,8 @@ AARU_EXPORT void *AARU_CALL aaruf_create(const char *filepath, const uint32_t me
ctx->use_zstd = parsed_options.zstd;
ctx->zstd_level = parsed_options.zstd_level;
ctx->num_threads = parsed_options.num_threads;
- if(ctx->deduplicate)
+ /* Flux-only images have no sector data to deduplicate against. */
+ if(ctx->deduplicate && !ctx->no_user_data_ddt)
ctx->sector_hash_map = create_map(ctx->user_data_ddt_header.blocks * 25 / 100); // 25% of total sectors
ctx->rewinded = false;
@@ -581,10 +603,12 @@ AARU_EXPORT void *AARU_CALL aaruf_create(const char *filepath, const uint32_t me
ctx->is_writing = true;
ctx->finalize_write = aaruf_finalize_write;
- // Initialize dirty flags - all true by default for new images
- ctx->dirty_secondary_ddt = true;
- ctx->dirty_primary_ddt = true;
- ctx->dirty_single_level_ddt = true;
+ // Initialize dirty flags - all true by default for new images.
+ // Flux-only images (no DDT) leave the DDT dirty flags cleared so finalize never
+ // attempts to serialize a non-existent table.
+ ctx->dirty_secondary_ddt = !ctx->no_user_data_ddt;
+ ctx->dirty_primary_ddt = !ctx->no_user_data_ddt;
+ ctx->dirty_single_level_ddt = !ctx->no_user_data_ddt;
ctx->dirty_checksum_block = true;
ctx->dirty_tracks_block = true;
ctx->dirty_mode2_subheaders_block = true;
diff --git a/src/open.c b/src/open.c
index 4f6e24b..2554f4e 100644
--- a/src/open.c
+++ b/src/open.c
@@ -543,6 +543,7 @@ AARU_EXPORT void *AARU_CALL aaruf_open(const char *filepath, const bool resume_m
}
bool found_user_data_ddt = false;
+ bool has_flux_block = false;
ctx->image_info.ImageSize = 0;
for(i = 0; i < utarray_len(index_entries); i++)
{
@@ -663,6 +664,7 @@ AARU_EXPORT void *AARU_CALL aaruf_open(const char *filepath, const bool resume_m
// Store the FluxDataBlock offset for lazy loading
// Don't read flux entries during open - load them on-demand when actually needed
// This avoids unnecessary I/O if flux data is never accessed
+ has_flux_block = true;
break;
default:
TRACE("Unhandled block type %4.4s with data type %d is indexed to be at %" PRIu64 "",
@@ -675,12 +677,19 @@ AARU_EXPORT void *AARU_CALL aaruf_open(const char *filepath, const bool resume_m
if(!found_user_data_ddt)
{
- FATAL("Could not find user data deduplication table, aborting...");
- aaruf_close(ctx);
- aaruf_set_open_error(AARUF_ERROR_CANNOT_READ_INDEX);
+ if(!has_flux_block)
+ {
+ FATAL("Could not find user data deduplication table or flux data, aborting...");
+ aaruf_close(ctx);
+ aaruf_set_open_error(AARUF_ERROR_CANNOT_READ_INDEX);
- TRACE("Exiting aaruf_open() = NULL");
- return NULL;
+ TRACE("Exiting aaruf_open() = NULL");
+ return NULL;
+ }
+
+ TRACE("No user data DDT found but flux data present, opening as flux-only image");
+ ctx->no_user_data_ddt = true;
+ /* image_info.Sectors stays 0; biggestSectorSize stays 0; sector APIs will reject. */
}
if(ctx->header.biggestSectorSize != 0)
@@ -748,9 +757,12 @@ AARU_EXPORT void *AARU_CALL aaruf_open(const char *filepath, const bool resume_m
ctx->header.lastWrittenTime = get_filetime_uint64();
ctx->image_info.LastModificationTime = ctx->header.lastWrittenTime;
- // Calculate aligned next block position
+ // Calculate aligned next block position. For flux-only images there is no user-data DDT
+ // header, so use the alignment shift recorded in the container header instead.
+ const uint8_t resume_alignment_shift =
+ ctx->no_user_data_ddt ? ctx->header.blockAlignmentShift : ctx->user_data_ddt_header.blockAlignmentShift;
aaruf_fseek(ctx->imageStream, 0, SEEK_END);
- const uint64_t alignment_mask = (1ULL << ctx->user_data_ddt_header.blockAlignmentShift) - 1;
+ const uint64_t alignment_mask = (1ULL << resume_alignment_shift) - 1;
ctx->next_block_position = (uint64_t)aaruf_ftell(ctx->imageStream); // Start just after the header
ctx->next_block_position = ctx->next_block_position + alignment_mask & ~alignment_mask;
@@ -770,7 +782,8 @@ AARU_EXPORT void *AARU_CALL aaruf_open(const char *filepath, const bool resume_m
ctx->compression_enabled = parsed_options.compress;
ctx->lzma_dict_size = parsed_options.dictionary;
ctx->deduplicate = parsed_options.deduplicate;
- if(ctx->deduplicate)
+ /* Flux-only images have no DDT and therefore no sector deduplication map. */
+ if(ctx->deduplicate && !ctx->no_user_data_ddt)
ctx->sector_hash_map = create_map(ctx->user_data_ddt_header.blocks * 25 / 100); // 25% of total sectors
// Cannot checksum a resumed file
diff --git a/src/read.c b/src/read.c
index 7ddbef7..976bef5 100644
--- a/src/read.c
+++ b/src/read.c
@@ -396,6 +396,14 @@ AARU_EXPORT int32_t AARU_CALL aaruf_read_sector(void *context, const uint64_t se
return AARUF_ERROR_NOT_AARUFORMAT;
}
+ // Flux-only images carry no DDT; sector reads cannot be served.
+ if(ctx->no_user_data_ddt)
+ {
+ *sector_status = SectorStatusNotDumped;
+ TRACE("Exiting aaruf_read_sector() = AARUF_ERROR_USER_DATA_NOT_PRESENT (flux-only image)");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
@@ -1472,6 +1480,14 @@ AARU_EXPORT int32_t AARU_CALL aaruf_read_sector_long(void *context, const uint64
return AARUF_ERROR_NOT_AARUFORMAT;
}
+ // Flux-only images carry no DDT; sector reads cannot be served.
+ if(ctx->no_user_data_ddt)
+ {
+ if(sector_status != NULL) *sector_status = SectorStatusNotDumped;
+ TRACE("Exiting aaruf_read_sector_long() = AARUF_ERROR_USER_DATA_NOT_PRESENT (flux-only image)");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
@@ -2275,6 +2291,13 @@ AARU_EXPORT int32_t AARU_CALL aaruf_read_sector_tag(const void *context, const u
return AARUF_ERROR_NOT_AARUFORMAT;
}
+ // Flux-only images carry no DDT and therefore no per-sector tags.
+ if(ctx->no_user_data_ddt)
+ {
+ TRACE("Exiting aaruf_read_sector_tag() = AARUF_ERROR_USER_DATA_NOT_PRESENT (flux-only image)");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
diff --git a/src/verify.c b/src/verify.c
index fec2168..bd5f550 100644
--- a/src/verify.c
+++ b/src/verify.c
@@ -226,13 +226,18 @@ AARU_EXPORT int32_t AARU_CALL aaruf_verify_image(void *context)
}
uint64_t crc_length;
- const unsigned int entry_count = utarray_len(index_entries);
+ const unsigned int entry_count = utarray_len(index_entries);
+ bool has_ddt_block = false;
+ bool has_flux_block = false;
for(unsigned int i = 0; i < entry_count; i++)
{
IndexEntry *entry = utarray_eltptr(index_entries, i);
TRACE("Checking block with type %4.4s at position %" PRIu64, (char *)&entry->blockType, entry->offset);
+ if(entry->blockType == DeDuplicationTable || entry->blockType == DeDuplicationTable2) has_ddt_block = true;
+ if(entry->blockType == FluxDataBlock) has_flux_block = true;
+
if(aaruf_fseek(ctx->imageStream, (aaru_off_t)entry->offset, SEEK_SET) != 0)
{
FATAL("Could not seek to block at offset %" PRIu64, entry->offset);
@@ -468,6 +473,15 @@ AARU_EXPORT int32_t AARU_CALL aaruf_verify_image(void *context)
}
}
+ /* An image must contain either a user-data DDT or a flux data block (flux-only images are
+ * legitimate). If neither is present the index is structurally invalid. */
+ if(!has_ddt_block && !has_flux_block)
+ {
+ FATAL("Image contains neither a user-data DDT nor a flux data block");
+ status = AARUF_ERROR_CANNOT_READ_INDEX;
+ goto cleanup;
+ }
+
status = AARUF_STATUS_OK;
cleanup:
diff --git a/src/write.c b/src/write.c
index 257048f..2227636 100644
--- a/src/write.c
+++ b/src/write.c
@@ -134,6 +134,15 @@ AARU_EXPORT int32_t AARU_CALL aaruf_write_sector(void *context, uint64_t sector_
return AARUF_READ_ONLY;
}
+ // Flux-only images carry no DDT and no sector storage.
+ if(ctx->no_user_data_ddt)
+ {
+ FATAL("Cannot write sector to flux-only image (no user-data DDT)");
+
+ TRACE("Exiting aaruf_write_sector() = AARUF_ERROR_USER_DATA_NOT_PRESENT");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
@@ -655,6 +664,15 @@ AARU_EXPORT int32_t AARU_CALL aaruf_write_sector_long(void *context, uint64_t se
return AARUF_READ_ONLY;
}
+ // Flux-only images carry no DDT and no sector storage.
+ if(ctx->no_user_data_ddt)
+ {
+ FATAL("Cannot write long sector to flux-only image (no user-data DDT)");
+
+ TRACE("Exiting aaruf_write_sector_long() = AARUF_ERROR_USER_DATA_NOT_PRESENT");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
@@ -2305,6 +2323,15 @@ AARU_EXPORT int32_t AARU_CALL aaruf_write_sector_tag(void *context, const uint64
return AARUF_READ_ONLY;
}
+ // Flux-only images carry no DDT and no per-sector tag storage.
+ if(ctx->no_user_data_ddt)
+ {
+ FATAL("Cannot write sector tag to flux-only image (no user-data DDT)");
+
+ TRACE("Exiting aaruf_write_sector_tag() = AARUF_ERROR_USER_DATA_NOT_PRESENT");
+ return AARUF_ERROR_USER_DATA_NOT_PRESENT;
+ }
+
if(negative && sector_address > ctx->user_data_ddt_header.negative)
{
FATAL("Sector address out of bounds");
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 79f58e4..d2f6471 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -104,6 +104,7 @@ add_executable(tests_run
mode2_errored.cpp
reed_solomon.cpp
erasure_coding.cpp
+ flux_only_image.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../src/lib/aes128.c
${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_crypto.c
${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_encryption_map.c
diff --git a/tests/flux_only_image.cpp b/tests/flux_only_image.cpp
new file mode 100644
index 0000000..7a7003b
--- /dev/null
+++ b/tests/flux_only_image.cpp
@@ -0,0 +1,176 @@
+/*
+ * This file is part of the Aaru Data Preservation Suite.
+ * Copyright (c) 2019-2026 Natalia Portillo.
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../include/aaruformat.h"
+#include "gtest/gtest.h"
+
+namespace
+{
+/// Fill a buffer with a deterministic, easy-to-validate pattern.
+void fill_pattern(uint8_t *buf, size_t length, uint32_t seed)
+{
+ for(size_t i = 0; i < length; ++i) buf[i] = static_cast((seed + i * 7u) & 0xFF);
+}
+} // namespace
+
+class FluxOnlyImageFixture : public testing::Test
+{
+public:
+ FluxOnlyImageFixture() = default;
+
+protected:
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/// Create a flux-only image (all sector counts zero), write a flux capture, close, reopen, verify
+/// and read back the flux capture. Sector reads must be rejected with AARUF_ERROR_USER_DATA_NOT_PRESENT.
+TEST_F(FluxOnlyImageFixture, create_open_read_verify_flux_only)
+{
+ constexpr const char *kPath = "test_flux_only.aif";
+
+ // Flux-only intent: user_sectors=0, negative=0, overflow=0, is_tape=false.
+ void *context = aaruf_create(kPath, /*media_type=*/1, /*sector_size=*/512, /*user_sectors=*/0, /*negative=*/0,
+ /*overflow=*/0, "compress=false",
+ reinterpret_cast("gtest"), 5, 0, 0, /*is_tape=*/false);
+ ASSERT_NE(context, nullptr) << "Failed to create flux-only image";
+
+ constexpr uint32_t kDataLen = 4096;
+ constexpr uint32_t kIndexLen = 64;
+ uint8_t data[kDataLen];
+ uint8_t index[kIndexLen];
+ fill_pattern(data, kDataLen, 0xA5u);
+ fill_pattern(index, kIndexLen, 0x5Au);
+
+ int32_t flux_write = aaruf_write_flux_capture(context,
+ /*head=*/0, /*track=*/0, /*subtrack=*/0,
+ /*capture_index=*/0,
+ /*data_resolution=*/25000000ULL,
+ /*index_resolution=*/25000000ULL, data, kDataLen, index, kIndexLen);
+ ASSERT_EQ(flux_write, AARUF_STATUS_OK) << "Failed to write flux capture";
+
+ // Sector writes against a flux-only image must be rejected, not silently accepted.
+ uint8_t sector[512];
+ fill_pattern(sector, sizeof(sector), 0u);
+ const int32_t sector_write =
+ aaruf_write_sector(context, /*sector_address=*/0, /*negative=*/false, sector, SectorStatusDumped, 512);
+ EXPECT_EQ(sector_write, AARUF_ERROR_USER_DATA_NOT_PRESENT)
+ << "Sector write to flux-only image should be rejected";
+
+ int32_t close_result = aaruf_close(context);
+ ASSERT_EQ(close_result, AARUF_STATUS_OK) << "Failed to close flux-only image";
+
+ context = aaruf_open(kPath, /*resume=*/false, /*options=*/nullptr);
+ ASSERT_NE(context, nullptr) << "Failed to reopen flux-only image";
+
+ EXPECT_EQ(aaruf_verify_image(context), AARUF_STATUS_OK)
+ << "Flux-only image should pass verification";
+
+ uint32_t read_index_len = kIndexLen;
+ uint32_t read_data_len = kDataLen;
+ uint8_t read_index[kIndexLen];
+ uint8_t read_data[kDataLen];
+ int32_t flux_read = aaruf_read_flux_capture(context, /*head=*/0, /*track=*/0, /*subtrack=*/0,
+ /*capture_index=*/0, read_index, &read_index_len, read_data,
+ &read_data_len);
+ ASSERT_EQ(flux_read, AARUF_STATUS_OK) << "Failed to read back flux capture";
+ EXPECT_EQ(read_data_len, kDataLen) << "Unexpected flux data length";
+ EXPECT_EQ(read_index_len, kIndexLen) << "Unexpected flux index length";
+ EXPECT_EQ(memcmp(read_data, data, kDataLen), 0) << "Flux data round-trip mismatch";
+ EXPECT_EQ(memcmp(read_index, index, kIndexLen), 0) << "Flux index round-trip mismatch";
+
+ // Sector reads against a flux-only image must surface the dedicated error code.
+ uint32_t sector_length = sizeof(sector);
+ uint8_t sector_status = 0;
+ int32_t sector_read =
+ aaruf_read_sector(context, /*sector_address=*/0, /*negative=*/false, sector, §or_length, §or_status);
+ EXPECT_EQ(sector_read, AARUF_ERROR_USER_DATA_NOT_PRESENT)
+ << "Sector read on flux-only image must return AARUF_ERROR_USER_DATA_NOT_PRESENT";
+ EXPECT_EQ(sector_status, SectorStatusNotDumped)
+ << "Sector status should report not-dumped for flux-only images";
+
+ sector_length = sizeof(sector);
+ sector_status = 0;
+ int32_t sector_long_read = aaruf_read_sector_long(context, /*sector_address=*/0, /*negative=*/false, sector,
+ §or_length, §or_status);
+ EXPECT_EQ(sector_long_read, AARUF_ERROR_USER_DATA_NOT_PRESENT)
+ << "Long sector read on flux-only image must return AARUF_ERROR_USER_DATA_NOT_PRESENT";
+
+ uint32_t tag_length = sizeof(sector);
+ int32_t tag_read =
+ aaruf_read_sector_tag(context, /*sector_address=*/0, /*negative=*/false, sector, &tag_length,
+ kSectorTagCdSubchannel);
+ EXPECT_EQ(tag_read, AARUF_ERROR_USER_DATA_NOT_PRESENT)
+ << "Sector tag read on flux-only image must return AARUF_ERROR_USER_DATA_NOT_PRESENT";
+
+ close_result = aaruf_close(context);
+ EXPECT_EQ(close_result, AARUF_STATUS_OK) << "Failed to close reopened flux-only image";
+
+ unlink(kPath);
+}
+
+/// A standard image (with sectors) that has no flux data should still respond to flux reads with
+/// AARUF_ERROR_FLUX_DATA_NOT_FOUND. Regression check for the "DDT-present, flux-absent" path.
+TEST_F(FluxOnlyImageFixture, regular_image_flux_read_returns_not_found)
+{
+ constexpr const char *kPath = "test_regular_no_flux.aif";
+ constexpr size_t kTotalSectors = 16;
+
+ void *context = aaruf_create(kPath, /*media_type=*/1, /*sector_size=*/512, kTotalSectors, /*negative=*/0,
+ /*overflow=*/0, "compress=false",
+ reinterpret_cast("gtest"), 5, 0, 0, /*is_tape=*/false);
+ ASSERT_NE(context, nullptr) << "Failed to create regular image";
+
+ uint8_t sector[512];
+ fill_pattern(sector, sizeof(sector), 0x42u);
+ for(size_t i = 0; i < kTotalSectors; ++i)
+ {
+ const int32_t write_result = aaruf_write_sector(context, i, false, sector, SectorStatusDumped, 512);
+ ASSERT_EQ(write_result, AARUF_STATUS_OK) << "Failed to write sector " << i;
+ }
+
+ int32_t close_result = aaruf_close(context);
+ ASSERT_EQ(close_result, AARUF_STATUS_OK) << "Failed to close regular image";
+
+ context = aaruf_open(kPath, false, nullptr);
+ ASSERT_NE(context, nullptr) << "Failed to reopen regular image";
+
+ EXPECT_EQ(aaruf_verify_image(context), AARUF_STATUS_OK) << "Regular image should pass verification";
+
+ uint8_t read_data[16];
+ uint8_t read_index[16];
+ uint32_t read_data_len = sizeof(read_data);
+ uint32_t read_index_len = sizeof(read_index);
+
+ int32_t flux_read = aaruf_read_flux_capture(context, 0, 0, 0, 0, read_index, &read_index_len, read_data,
+ &read_data_len);
+ EXPECT_EQ(flux_read, AARUF_ERROR_FLUX_DATA_NOT_FOUND)
+ << "Regular image without flux should return AARUF_ERROR_FLUX_DATA_NOT_FOUND";
+
+ close_result = aaruf_close(context);
+ EXPECT_EQ(close_result, AARUF_STATUS_OK) << "Failed to close reopened regular image";
+
+ unlink(kPath);
+}