/*
* 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 .
*/
/**
* @file mode2_nocrc.cpp
* @brief Regression tests for Mode 2 Form 2 NoCrc EDC fix.
*
* Covers fix/mode2-form2-nocrc-edc: when a Mode 2 Form 2 sector has zero
* EDC on write (SectorStatusMode2Form2NoCrc), the read path must return
* bytes 2348..2351 as zero via memset(data + 2348, 0, 4).
*
* Without the fix, those 4 bytes contain uninitialized data from the
* read buffer, which silently corrupts the sector on roundtrip.
*/
#include
#include
#include
#include
#include "../include/aaruformat.h"
#include "gtest/gtest.h"
namespace
{
/// CD sync pattern: 0x00, 10x 0xFF, 0x00.
constexpr uint8_t kSyncPattern[12] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
/// Media type for CD-ROM XA (Yellow Book), same as existing cdmode2 fixtures.
constexpr uint32_t kMediaTypeCdRomXa = 16;
/// CD sector size (user data portion for Mode 2).
constexpr uint32_t kSectorSizeMode2 = 2048;
/// Full raw CD sector size.
constexpr uint32_t kRawSectorSize = 2352;
/// EDC offset within a 2352-byte Mode 2 Form 2 sector.
constexpr int kEdcOffset = 2348;
/// User data offset within a Mode 2 sector (after sync + header + subheader).
constexpr int kUserDataOffset = 24;
/// User data length for Mode 2 Form 2.
constexpr int kUserDataLenForm2 = 2324;
/**
* Build a valid Mode 2 Form 2 CD sector (2352 bytes) for the given LBA.
*
* Layout: Sync(12) + Header(4) + Subheader(8) + UserData(2324) + EDC(4)
* The subheader has bit 5 set in both copies (Form 2).
* User data is filled with @p fill byte. EDC is set to @p edc.
*/
void BuildMode2Form2Sector(uint8_t *sector, const int64_t lba, const uint8_t fill, const uint32_t edc)
{
memset(sector, 0, kRawSectorSize);
// Sync (12 bytes)
memcpy(sector, kSyncPattern, sizeof(kSyncPattern));
// Header: MSF in BCD + Mode 2
const int64_t abs_frame = lba + 150;
const auto minute = static_cast(abs_frame / (60 * 75));
const auto second = static_cast((abs_frame / 75) % 60);
const auto frame = static_cast(abs_frame % 75);
sector[12] = static_cast((minute / 10) << 4 | (minute % 10));
sector[13] = static_cast((second / 10) << 4 | (second % 10));
sector[14] = static_cast((frame / 10) << 4 | (frame % 10));
sector[15] = 0x02; // Mode 2
// Subheader copy 1: [file_number, channel, submode, coding_info]
sector[18] = 0x20; // submode bit 5 = Form 2
// Subheader copy 2 (identical)
sector[22] = 0x20;
// User data (2324 bytes at offsets 24..2347)
memset(sector + kUserDataOffset, fill, kUserDataLenForm2);
// EDC (4 bytes at offsets 2348..2351)
memcpy(sector + kEdcOffset, &edc, sizeof(edc));
}
} // namespace
class Mode2NoCrcFixture : public testing::Test
{
};
/**
* Core regression test: NoCrc sectors must read back with zero EDC bytes.
*
* Writes Mode 2 Form 2 sectors with EDC=0 (NoCrc condition), then reads
* them back into a poisoned buffer and verifies bytes 2348..2351 are zero.
*/
TEST_F(Mode2NoCrcFixture, NoCrcEdcBytesAreZero)
{
constexpr size_t kSectors = 4;
const char *kFilename = "test_mode2_nocrc.aif";
// --- Create image ---
void *ctx = aaruf_create(kFilename, kMediaTypeCdRomXa, kSectorSizeMode2, kSectors, 0, 0,
"deduplicate=false;compress=false",
reinterpret_cast("gtest"), 5, 0, 0, false);
ASSERT_NE(ctx, nullptr) << "Failed to create image";
TrackEntry track{};
track.sequence = 1;
track.type = kTrackTypeCdMode2Formless;
track.start = 0;
track.end = static_cast(kSectors) - 1;
track.session = 1;
track.flags = 0x04; // Data track
ASSERT_EQ(aaruf_set_tracks(ctx, &track, 1), AARUF_STATUS_OK) << "Failed to set tracks";
// Write 4 sectors, each with zero EDC → triggers NoCrc status in DDT
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t sector[kRawSectorSize];
BuildMode2Form2Sector(sector, static_cast(i), static_cast(0x41 + i), /*edc=*/0);
const int32_t rc = aaruf_write_sector_long(ctx, i, false, sector, SectorStatusDumped, kRawSectorSize);
ASSERT_EQ(rc, AARUF_STATUS_OK) << "Failed to write sector " << i;
}
ASSERT_EQ(aaruf_close(ctx), AARUF_STATUS_OK) << "Failed to close image after write";
// --- Reopen & verify ---
ctx = aaruf_open(kFilename, false, nullptr);
ASSERT_NE(ctx, nullptr) << "Failed to reopen image";
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t buffer[kRawSectorSize];
memset(buffer, 0xCC, sizeof(buffer)); // Poison buffer to expose uninitialized bytes
uint32_t length = sizeof(buffer);
uint8_t status = 0;
ASSERT_EQ(aaruf_read_sector_long(ctx, i, false, buffer, &length, &status), AARUF_STATUS_OK)
<< "Failed to read sector " << i;
ASSERT_EQ(length, kRawSectorSize) << "Unexpected length for sector " << i;
// THE FIX: bytes 2348..2351 must be zero, not uninitialized.
// Without fix/mode2-form2-nocrc-edc these would be 0xCC (the poison byte).
EXPECT_EQ(buffer[2348], 0x00) << "EDC byte 0 non-zero in NoCrc sector " << i;
EXPECT_EQ(buffer[2349], 0x00) << "EDC byte 1 non-zero in NoCrc sector " << i;
EXPECT_EQ(buffer[2350], 0x00) << "EDC byte 2 non-zero in NoCrc sector " << i;
EXPECT_EQ(buffer[2351], 0x00) << "EDC byte 3 non-zero in NoCrc sector " << i;
// Verify user data survived roundtrip
const uint8_t fill = static_cast(0x41 + i);
for(int j = kUserDataOffset; j < kUserDataOffset + kUserDataLenForm2; ++j)
EXPECT_EQ(buffer[j], fill) << "User data mismatch at offset " << j << " in sector " << i;
// Verify subheader Form 2 bit preserved (at least one copy)
EXPECT_TRUE((buffer[18] & 0x20) != 0 || (buffer[22] & 0x20) != 0)
<< "Form 2 subheader bit lost in sector " << i;
}
EXPECT_EQ(aaruf_close(ctx), AARUF_STATUS_OK);
remove(kFilename);
}
/**
* Sanity check: sectors with valid EDC must not get zeroed on roundtrip.
*
* Writes Mode 2 Form 2 sectors with a correctly computed EDC, reads them
* back, and verifies the EDC is non-zero and the suffix is valid. This
* confirms the NoCrc test above is meaningful — only zero-EDC sectors
* receive the memset treatment.
*/
TEST_F(Mode2NoCrcFixture, ValidEdcSurvivesRoundtrip)
{
constexpr size_t kSectors = 2;
const char *kFilename = "test_mode2_valid_edc.aif";
void *ctx = aaruf_create(kFilename, kMediaTypeCdRomXa, kSectorSizeMode2, kSectors, 0, 0,
"deduplicate=false;compress=false",
reinterpret_cast("gtest"), 5, 0, 0, false);
ASSERT_NE(ctx, nullptr);
TrackEntry track{};
track.sequence = 1;
track.type = kTrackTypeCdMode2Formless;
track.start = 0;
track.end = static_cast(kSectors) - 1;
track.session = 1;
track.flags = 0x04;
ASSERT_EQ(aaruf_set_tracks(ctx, &track, 1), AARUF_STATUS_OK);
// Compute valid EDC using the library's own function
void *ecc_ctx = aaruf_ecc_cd_init();
ASSERT_NE(ecc_ctx, nullptr);
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t sector[kRawSectorSize];
BuildMode2Form2Sector(sector, static_cast(i), static_cast(0x55 + i), /*edc=*/0);
// Compute valid EDC over subheader + user data (bytes 16..2347)
const uint32_t edc = aaruf_edc_cd_compute(ecc_ctx, 0, sector, 0x91C, 0x10);
memcpy(sector + kEdcOffset, &edc, sizeof(edc));
ASSERT_EQ(aaruf_write_sector_long(ctx, i, false, sector, SectorStatusDumped, kRawSectorSize), AARUF_STATUS_OK)
<< "Failed to write sector " << i;
}
aaruf_ecc_cd_free(ecc_ctx);
ASSERT_EQ(aaruf_close(ctx), AARUF_STATUS_OK);
// --- Reopen & verify ---
ctx = aaruf_open(kFilename, false, nullptr);
ASSERT_NE(ctx, nullptr);
ecc_ctx = aaruf_ecc_cd_init();
ASSERT_NE(ecc_ctx, nullptr);
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t buffer[kRawSectorSize];
memset(buffer, 0xCC, sizeof(buffer));
uint32_t length = sizeof(buffer);
uint8_t status = 0;
ASSERT_EQ(aaruf_read_sector_long(ctx, i, false, buffer, &length, &status), AARUF_STATUS_OK)
<< "Failed to read sector " << i;
ASSERT_EQ(length, kRawSectorSize);
// EDC must be non-zero (valid, not NoCrc)
uint32_t read_edc = 0;
memcpy(&read_edc, buffer + kEdcOffset, sizeof(read_edc));
EXPECT_NE(read_edc, 0U) << "EDC should be non-zero for valid sector " << i;
// Recompute expected EDC from the read-back sector and compare.
// (aaruf_ecc_cd_is_suffix_correct_mode2 also checks ECC P/Q which
// Form 2 sectors don't have — the ECC area IS user data for Form 2.)
const uint32_t expected_edc = aaruf_edc_cd_compute(ecc_ctx, 0, buffer, 0x91C, 0x10);
EXPECT_EQ(read_edc, expected_edc) << "EDC mismatch for sector " << i;
// Verify user data
const uint8_t fill = static_cast(0x55 + i);
for(int j = kUserDataOffset; j < kUserDataOffset + kUserDataLenForm2; ++j)
EXPECT_EQ(buffer[j], fill) << "User data mismatch at offset " << j << " in sector " << i;
}
aaruf_ecc_cd_free(ecc_ctx);
EXPECT_EQ(aaruf_close(ctx), AARUF_STATUS_OK);
remove(kFilename);
}
/**
* Mixed test: NoCrc and valid-EDC sectors in the same image.
*
* Verifies the DDT correctly tracks per-sector suffix status when both
* NoCrc (SectorStatusMode2Form2NoCrc) and valid (SectorStatusMode2Form2Ok)
* sectors coexist in a single track.
*/
TEST_F(Mode2NoCrcFixture, MixedNoCrcAndValidEdcPerSector)
{
constexpr size_t kSectors = 8;
const char *kFilename = "test_mode2_mixed.aif";
void *ctx = aaruf_create(kFilename, kMediaTypeCdRomXa, kSectorSizeMode2, kSectors, 0, 0,
"deduplicate=false;compress=false",
reinterpret_cast("gtest"), 5, 0, 0, false);
ASSERT_NE(ctx, nullptr);
TrackEntry track{};
track.sequence = 1;
track.type = kTrackTypeCdMode2Formless;
track.start = 0;
track.end = static_cast(kSectors) - 1;
track.session = 1;
track.flags = 0x04;
ASSERT_EQ(aaruf_set_tracks(ctx, &track, 1), AARUF_STATUS_OK);
void *ecc_ctx = aaruf_ecc_cd_init();
ASSERT_NE(ecc_ctx, nullptr);
// Even sectors: NoCrc (EDC=0). Odd sectors: valid EDC.
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t sector[kRawSectorSize];
BuildMode2Form2Sector(sector, static_cast(i), static_cast(0x30 + i), /*edc=*/0);
if(i % 2 != 0)
{
// Odd sectors: compute and set valid EDC
const uint32_t edc = aaruf_edc_cd_compute(ecc_ctx, 0, sector, 0x91C, 0x10);
memcpy(sector + kEdcOffset, &edc, sizeof(edc));
}
// Even sectors: EDC remains 0 → NoCrc
ASSERT_EQ(aaruf_write_sector_long(ctx, i, false, sector, SectorStatusDumped, kRawSectorSize), AARUF_STATUS_OK)
<< "write sector " << i;
}
aaruf_ecc_cd_free(ecc_ctx);
ASSERT_EQ(aaruf_close(ctx), AARUF_STATUS_OK);
// --- Reopen & verify ---
ctx = aaruf_open(kFilename, false, nullptr);
ASSERT_NE(ctx, nullptr);
ecc_ctx = aaruf_ecc_cd_init();
ASSERT_NE(ecc_ctx, nullptr);
for(size_t i = 0; i < kSectors; ++i)
{
uint8_t buffer[kRawSectorSize];
memset(buffer, 0xCC, sizeof(buffer));
uint32_t length = sizeof(buffer);
uint8_t status = 0;
ASSERT_EQ(aaruf_read_sector_long(ctx, i, false, buffer, &length, &status), AARUF_STATUS_OK)
<< "read sector " << i;
ASSERT_EQ(length, kRawSectorSize);
uint32_t read_edc = 0;
memcpy(&read_edc, buffer + kEdcOffset, sizeof(read_edc));
if(i % 2 == 0)
{
// NoCrc sector: EDC must be zero
EXPECT_EQ(read_edc, 0U) << "Even sector " << i << " should have zero EDC (NoCrc)";
}
else
{
// Valid EDC sector: EDC must be non-zero and match recomputation
EXPECT_NE(read_edc, 0U) << "Odd sector " << i << " should have non-zero EDC";
const uint32_t expected_edc = aaruf_edc_cd_compute(ecc_ctx, 0, buffer, 0x91C, 0x10);
EXPECT_EQ(read_edc, expected_edc) << "EDC mismatch for odd sector " << i;
}
// User data must be correct regardless of EDC status
const uint8_t fill = static_cast(0x30 + i);
for(int j = kUserDataOffset; j < kUserDataOffset + kUserDataLenForm2; ++j)
EXPECT_EQ(buffer[j], fill) << "User data mismatch at offset " << j << " in sector " << i;
}
aaruf_ecc_cd_free(ecc_ctx);
EXPECT_EQ(aaruf_close(ctx), AARUF_STATUS_OK);
remove(kFilename);
}