Add repair-cd-arena tool command

Recovers CD sector prefix/suffix arenas in a v2 image whose resumed dump
sessions left the final deduplication table referencing slots past the
shrunken data block. Scans the file for every prefix/suffix generation,
rebuilds a fresh compact arena and DDT2 for the final table by sourcing
each custom slot from the generation that held it, and rewrites the index.
Supports --dry-run to report recoverable/lost slots without writing.
This commit is contained in:
2026-09-01 14:56:21 +01:00
parent 29bdba9c66
commit c8de84871d
7 changed files with 604 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ add_executable(aaruformattool
cli_compare.c
convert.c
upgrade_ddt_to_alpha21.c
repair_cd_arena.c
inject_media_tag.c
ps3/ird.c
ps3/ird.h

View File

@@ -73,6 +73,7 @@ int cli_compare(const char *path1, const char *path2, bool use_long);
int convert_ec(const char *input_path, const char *output_path, bool use_long, uint16_t ec_k, uint16_t ec_m);
int verify_recover(const char *path);
int upgrade_ddt_to_alpha21(const char *path);
int repair_cd_arena(const char *path, bool dry_run);
int inject_media_tag(const char *tag_type, const char *media_tag_file, const char *image_file);
int convert_ps3(const char *input_path, const char *output_path, const char *disc_key_hex, const char *data1_key_hex,
const char *ird_path);

View File

@@ -213,6 +213,26 @@ int cmd_upgrade_ddt_to_alpha21(int argc, char *argv[])
return result;
}
int cmd_repair_cd_arena(int argc, char *argv[])
{
struct arg_lit *dry_run = arg_lit0(NULL, "dry-run", "Only report recoverable/lost slots, write nothing");
struct arg_str *filename = arg_str1(NULL, NULL, "<filename>", "Image to repair");
struct arg_end *end = arg_end(10);
void *argtable[] = {dry_run, filename, end};
if(arg_parse(argc, argv, argtable) > 0)
{
arg_print_errors(stderr, end, "repair-cd-arena");
usage_repair_cd_arena();
arg_freetable(argtable, sizeof(argtable) / sizeof(argtable[0]));
return -1;
}
const int result = repair_cd_arena(filename->sval[0], dry_run->count > 0);
arg_freetable(argtable, sizeof(argtable) / sizeof(argtable[0]));
return result;
}
int cmd_inject_media_tag(int argc, char *argv[])
{
struct arg_str *tag_type = arg_str1(NULL, NULL, "<tag-type>", "Media tag type to inject");
@@ -318,6 +338,7 @@ Command commands[] = {
{ "cli-compare", cmd_cli_compare},
{ "convert", cmd_convert},
{"upgrade-ddt-to-alpha21", cmd_upgrade_ddt_to_alpha21},
{ "repair-cd-arena", cmd_repair_cd_arena},
{ "inject-media-tag", cmd_inject_media_tag},
{ "convert-ps3", cmd_convert_ps3},
{ "convert-wiiu", cmd_convert_wiiu},

View File

@@ -44,6 +44,7 @@ int cmd_compare(int argc, char *argv[]);
int cmd_cli_compare(int argc, char *argv[]);
int cmd_convert(int argc, char *argv[]);
int cmd_upgrade_ddt_to_alpha21(int argc, char *argv[]);
int cmd_repair_cd_arena(int argc, char *argv[]);
int cmd_inject_media_tag(int argc, char *argv[]);
int cmd_convert_ps3(int argc, char *argv[]);
int cmd_convert_ngcw(int argc, char *argv[]);

566
tool/repair_cd_arena.c Normal file
View File

@@ -0,0 +1,566 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*
* repair-cd-arena
*
* Recovers CD sector prefix/suffix arenas in AaruFormat v2 images damaged by the resume bug in which a
* resumed dump session restarted the custom prefix/suffix arena at offset 0. That left the final
* deduplication table (DDT2) referencing slot indexes that point past the (shrunken) final data block,
* so errored sectors read back as garbage.
*
* Such an image still physically contains every dump session's prefix/suffix data block and DDT2 (the old
* generations are merely orphaned, not overwritten). This tool scans the file for all generations, and for
* every custom entry in the final DDT2 finds the generation whose DDT2 held that exact (status,index) value
* and whose data block actually contains the slot, then rebuilds a fresh compact arena + DDT2 with
* renumbered indexes and rewrites the index to point at them.
*/
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <aaruformat.h>
#include <aaruformat/consts.h>
#include <aaruformat/enums.h>
#include <aaruformat/structs/data.h>
#include <aaruformat/structs/ddt.h>
#include <aaruformat/structs/header.h>
#include <aaruformat/structs/index.h>
#include "aaruformattool.h"
#include "usage.h"
#define PREFIX_SLOT 16u
#define SUFFIX_SLOT 288u
/// A decoded prefix/suffix generation: one data block and its matching DDT2, both for the same data type.
typedef struct
{
uint16_t data_type; // 69 (prefix) or 70 (suffix)
uint64_t block_offset; // file offset of the DataBlock (for ordering: session order == file order)
uint8_t *block; // decoded arena bytes
uint64_t block_length; // arena length in bytes
uint64_t ddt_offset; // file offset of the DDT2
uint64_t *ddt; // decoded DDT2 entries (one uint64_t per internal sector)
uint64_t ddt_entries; // number of DDT2 entries
} Generation;
static uint32_t status_of(const uint64_t entry) { return (uint32_t)(entry >> 60); }
static uint64_t index_of(const uint64_t entry) { return entry & 0x0FFFFFFFFFFFFFFFULL; }
/// True when the entry references a stored slot (custom bytes), false for the inline/regenerable statuses.
static bool is_custom(const uint64_t entry)
{
switch(status_of(entry))
{
case SectorStatusNotDumped:
case SectorStatusMode1Correct:
case SectorStatusMode2Form1Ok:
case SectorStatusMode2Form2Ok:
case SectorStatusMode2Form2NoCrc: return false;
default: return true;
}
}
/// Decodes a DataBlock/DDT2 payload (compression none/lzma/zstd) into a freshly allocated buffer whose CRC64
/// matches @p crc64. The compressed payload starts at @p payload in the in-memory image, @p cmp_length long.
static uint8_t *decode_payload(const uint8_t *payload, const uint16_t compression, const uint64_t cmp_length,
const uint64_t length, const uint64_t crc64)
{
uint8_t *out = NULL;
if(compression == kCompressionNone)
{
out = malloc(length);
if(out != NULL) memcpy(out, payload, length);
}
else if(compression == kCompressionLzma)
{
if(cmp_length < LZMA_PROPERTIES_LENGTH) return NULL;
out = malloc(length);
if(out != NULL)
{
size_t out_size = length;
size_t src_size = cmp_length - LZMA_PROPERTIES_LENGTH;
if(aaruf_lzma_decode_buffer(out, &out_size, payload + LZMA_PROPERTIES_LENGTH, &src_size,
payload, LZMA_PROPERTIES_LENGTH) != 0 ||
out_size != length)
{
free(out);
out = NULL;
}
}
}
else if(compression == kCompressionZstd)
{
out = malloc(length);
if(out != NULL && aaruf_zstd_decode_buffer(out, length, payload, cmp_length) != length)
{
free(out);
out = NULL;
}
}
if(out != NULL && aaruf_crc64_data(out, (uint32_t)length) != crc64)
{
free(out);
return NULL;
}
return out;
}
/// Scans the in-memory image for prefix/suffix DataBlock and DDT2 blocks, decoding and CRC-validating each,
/// and pairs each DDT2 with the nearest preceding data block of the same type into a Generation.
static Generation *scan_generations(const uint8_t *img, const uint64_t file_size, size_t *out_count)
{
// Collect decoded data blocks and DDT2s separately, then pair by (type, order).
typedef struct
{
uint16_t type;
uint64_t offset;
uint8_t *data;
uint64_t length;
} Decoded;
Decoded *blocks = NULL, *ddts = NULL;
size_t nblocks = 0, nddts = 0, capb = 0, capd = 0;
for(uint64_t pos = 0; pos + sizeof(BlockHeader) <= file_size; pos++)
{
uint32_t ident;
memcpy(&ident, img + pos, sizeof(ident));
if(ident == DataBlock)
{
BlockHeader bh;
memcpy(&bh, img + pos, sizeof(bh));
if(bh.type != kDataTypeCdSectorPrefix && bh.type != kDataTypeCdSectorSuffix) continue;
if(bh.length == 0 || bh.length > file_size) continue;
uint64_t payload = pos + sizeof(BlockHeader);
if(bh.compression == kCompressionLzma) payload += LZMA_PROPERTIES_LENGTH;
if(payload + bh.cmpLength > file_size) continue;
uint8_t *data = decode_payload(img + payload, bh.compression, bh.cmpLength, bh.length, bh.crc64);
if(data == NULL) continue;
if(nblocks == capb)
{
capb = capb ? capb * 2 : 8;
blocks = realloc(blocks, capb * sizeof(Decoded));
}
blocks[nblocks++] = (Decoded){bh.type, pos, data, bh.length};
}
else if(ident == DeDuplicationTable2)
{
DdtHeader2 dh;
if(pos + sizeof(DdtHeader2) > file_size) continue;
memcpy(&dh, img + pos, sizeof(dh));
if(dh.type != kDataTypeCdSectorPrefix && dh.type != kDataTypeCdSectorSuffix) continue;
if(dh.length == 0 || dh.length > file_size || (dh.length % sizeof(uint64_t)) != 0) continue;
uint64_t payload = pos + sizeof(DdtHeader2);
if(dh.compression == kCompressionLzma) payload += LZMA_PROPERTIES_LENGTH;
if(payload + dh.cmpLength > file_size) continue;
uint8_t *data = decode_payload(img + payload, dh.compression, dh.cmpLength, dh.length, dh.crc64);
if(data == NULL) continue;
if(nddts == capd)
{
capd = capd ? capd * 2 : 8;
ddts = realloc(ddts, capd * sizeof(Decoded));
}
ddts[nddts++] = (Decoded){dh.type, pos, data, dh.length};
}
}
// Pair every DDT2 with the closest preceding data block of the same type (they are written together).
Generation *gens = calloc(nddts, sizeof(Generation));
size_t ngens = 0;
for(size_t i = 0; i < nddts; i++)
{
Decoded *best = NULL;
for(size_t j = 0; j < nblocks; j++)
if(blocks[j].type == ddts[i].type && blocks[j].offset < ddts[i].offset &&
(best == NULL || blocks[j].offset > best->offset))
best = &blocks[j];
if(best == NULL) continue;
gens[ngens].data_type = ddts[i].type;
gens[ngens].block_offset = best->offset;
gens[ngens].block = best->data;
gens[ngens].block_length = best->length;
gens[ngens].ddt_offset = ddts[i].offset;
gens[ngens].ddt = (uint64_t *)ddts[i].data;
gens[ngens].ddt_entries = ddts[i].length / sizeof(uint64_t);
ngens++;
}
free(blocks);
free(ddts);
*out_count = ngens;
return gens;
}
/// Locates the slot bytes for one custom entry by walking generations for a matching (status,index) whose
/// block holds the slot. Returns a pointer into a generation's block, or NULL if unrecoverable.
static const uint8_t *find_slot(const Generation *gens, const size_t ngens, const uint16_t type,
const uint64_t sector, const uint64_t entry, const uint32_t slot_size)
{
const uint64_t idx = index_of(entry);
for(size_t g = 0; g < ngens; g++)
{
if(gens[g].data_type != type) continue;
if(sector >= gens[g].ddt_entries) continue;
if(gens[g].ddt[sector] != entry) continue;
if((idx + 1) * slot_size > gens[g].block_length) continue;
return gens[g].block + idx * slot_size;
}
return NULL;
}
/// Rebuilds one arena (prefix or suffix) from the final DDT2. On success returns the new compact arena and
/// fills @p new_ddt (caller-provided, entries long) with renumbered entries; reports recovered/lost counts.
static uint8_t *rebuild_arena(const Generation *gens, const size_t ngens, const uint16_t type,
const uint64_t *final_ddt, const uint64_t entries, const uint32_t slot_size,
uint64_t *new_ddt, uint64_t *out_length, uint64_t *recovered, uint64_t *lost)
{
uint64_t customs = 0;
for(uint64_t s = 0; s < entries; s++)
if(is_custom(final_ddt[s])) customs++;
uint8_t *arena = customs ? calloc(customs, slot_size) : NULL;
uint64_t next_slot = 0;
*recovered = 0;
*lost = 0;
for(uint64_t s = 0; s < entries; s++)
{
if(!is_custom(final_ddt[s]))
{
new_ddt[s] = final_ddt[s];
continue;
}
const uint8_t *slot = find_slot(gens, ngens, type, s, final_ddt[s], slot_size);
if(slot == NULL)
{
// Unrecoverable: mark NotDumped so the sector reads back as a gap instead of garbage.
new_ddt[s] = (uint64_t)SectorStatusNotDumped << 60;
(*lost)++;
continue;
}
memcpy(arena + next_slot * slot_size, slot, slot_size);
new_ddt[s] = ((uint64_t)status_of(final_ddt[s]) << 60) | next_slot;
next_slot++;
(*recovered)++;
}
*out_length = next_slot * slot_size;
return arena;
}
/// Appends a DataBlock (uncompressed) at an aligned EOF and returns its file offset (via @p out_offset).
static int append_data_block(FILE *fp, const uint16_t type, const uint8_t *data, const uint64_t length,
const uint8_t alignment_shift, uint64_t *out_offset)
{
fseek(fp, 0, SEEK_END);
uint64_t pos = (uint64_t)ftell(fp);
const uint64_t mask = (1ULL << alignment_shift) - 1;
pos = (pos + mask) & ~mask;
fseek(fp, (long)pos, SEEK_SET);
BlockHeader bh = {0};
bh.identifier = DataBlock;
bh.type = type;
bh.compression = kCompressionNone;
bh.sectorSize = type == kDataTypeCdSectorPrefix ? PREFIX_SLOT : SUFFIX_SLOT;
bh.cmpLength = (uint32_t)length;
bh.length = (uint32_t)length;
bh.crc64 = length ? aaruf_crc64_data(data, (uint32_t)length) : 0;
bh.cmpCrc64 = bh.crc64;
if(fwrite(&bh, sizeof(bh), 1, fp) != 1) return EIO;
if(length && fwrite(data, length, 1, fp) != 1) return EIO;
*out_offset = pos;
return 0;
}
/// Appends a DDT2 (uncompressed) at an aligned EOF, cloning @p templ's geometry, and returns its offset.
static int append_ddt2(FILE *fp, const DdtHeader2 *templ, const uint16_t type, const uint64_t *ddt,
const uint64_t entries, uint64_t *out_offset)
{
fseek(fp, 0, SEEK_END);
uint64_t pos = (uint64_t)ftell(fp);
const uint64_t mask = (1ULL << templ->blockAlignmentShift) - 1;
pos = (pos + mask) & ~mask;
fseek(fp, (long)pos, SEEK_SET);
const uint64_t length = entries * sizeof(uint64_t);
DdtHeader2 dh = *templ;
dh.identifier = DeDuplicationTable2;
dh.type = type;
dh.compression = kCompressionNone;
dh.entries = entries;
dh.cmpLength = length;
dh.length = length;
dh.crc64 = aaruf_crc64_data((const uint8_t *)ddt, (uint32_t)length);
dh.cmpCrc64 = dh.crc64;
if(fwrite(&dh, sizeof(dh), 1, fp) != 1) return EIO;
if(length && fwrite(ddt, length, 1, fp) != 1) return EIO;
*out_offset = pos;
return 0;
}
int repair_cd_arena(const char *path, const bool dry_run)
{
print_banner();
printf("repair-cd-arena on '%s'%s\n\n", path, dry_run ? " (dry run)" : "");
FILE *fp = fopen(path, dry_run ? "rb" : "r+b");
if(fp == NULL)
{
printf("ERROR: cannot open '%s': %s\n", path, strerror(errno));
return errno;
}
AaruHeaderV2 header;
if(fread(&header, 1, sizeof(header), fp) != sizeof(header))
{
printf("ERROR: cannot read header\n");
fclose(fp);
return EIO;
}
if(header.identifier != AARU_MAGIC || header.imageMajorVersion != AARUF_VERSION_V2)
{
printf("ERROR: not an AaruFormat v2 image\n");
fclose(fp);
return EINVAL;
}
fseek(fp, 0, SEEK_END);
const uint64_t file_size = (uint64_t)ftell(fp);
// Load the whole image into memory for fast scanning/decoding.
uint8_t *img = malloc(file_size);
if(img == NULL)
{
printf("ERROR: cannot allocate %llu bytes for image\n", (unsigned long long)file_size);
fclose(fp);
return ENOMEM;
}
fseek(fp, 0, SEEK_SET);
if(fread(img, 1, file_size, fp) != file_size)
{
printf("ERROR: cannot read image into memory\n");
free(img);
fclose(fp);
return EIO;
}
// --- Read the current index (IndexBlock3 chain) ---
size_t index_count = 0, index_cap = 0;
IndexEntry *index = NULL;
uint64_t idx_off = header.indexOffset;
while(idx_off)
{
IndexHeader3 ih;
if(fseek(fp, (long)idx_off, SEEK_SET) != 0 || fread(&ih, 1, sizeof(ih), fp) != sizeof(ih) ||
ih.identifier != IndexBlock3)
{
printf("ERROR: cannot read index at %llu\n", (unsigned long long)idx_off);
free(index);
fclose(fp);
return EIO;
}
for(uint64_t i = 0; i < ih.entries; i++)
{
IndexEntry e;
if(fread(&e, 1, sizeof(e), fp) != sizeof(e)) break;
if(index_count == index_cap)
{
index_cap = index_cap ? index_cap * 2 : 32;
index = realloc(index, index_cap * sizeof(IndexEntry));
}
index[index_count++] = e;
}
idx_off = ih.previous;
}
// --- Locate the final prefix/suffix DDT2 the index references ---
uint64_t final_prefix_ddt_off = 0, final_suffix_ddt_off = 0;
for(size_t i = 0; i < index_count; i++)
if(index[i].blockType == DeDuplicationTable2)
{
if(index[i].dataType == kDataTypeCdSectorPrefix) final_prefix_ddt_off = index[i].offset;
else if(index[i].dataType == kDataTypeCdSectorSuffix)
final_suffix_ddt_off = index[i].offset;
}
if(final_prefix_ddt_off == 0 && final_suffix_ddt_off == 0)
{
printf("Image has no CD sector prefix/suffix DDT2; nothing to repair.\n");
free(index);
fclose(fp);
return 0;
}
// --- Scan all generations ---
size_t ngens = 0;
Generation *gens = scan_generations(img, file_size, &ngens);
printf("Found %zu prefix/suffix generations.\n", ngens);
int rc = 0;
// Process both arenas.
struct
{
uint16_t type;
uint64_t final_ddt_off;
uint32_t slot;
uint64_t new_block_off;
uint64_t new_ddt_off;
} jobs[2] = {
{kDataTypeCdSectorPrefix, final_prefix_ddt_off, PREFIX_SLOT, 0, 0},
{kDataTypeCdSectorSuffix, final_suffix_ddt_off, SUFFIX_SLOT, 0, 0},
};
for(int j = 0; j < 2; j++)
{
if(jobs[j].final_ddt_off == 0) continue;
// Find the final DDT2 among the scanned generations (by offset) and its header template.
const Generation *final_gen = NULL;
for(size_t g = 0; g < ngens; g++)
if(gens[g].data_type == jobs[j].type && gens[g].ddt_offset == jobs[j].final_ddt_off)
final_gen = &gens[g];
if(final_gen == NULL)
{
printf("WARNING: final %s DDT2 could not be decoded; skipping.\n",
jobs[j].type == kDataTypeCdSectorPrefix ? "prefix" : "suffix");
continue;
}
DdtHeader2 templ;
if(fseek(fp, (long)jobs[j].final_ddt_off, SEEK_SET) != 0 ||
fread(&templ, 1, sizeof(templ), fp) != sizeof(templ))
{
rc = EIO;
break;
}
const uint64_t entries = final_gen->ddt_entries;
uint64_t *new_ddt = malloc(entries * sizeof(uint64_t));
uint64_t reclen = 0, recovered = 0, lost = 0;
uint8_t *arena =
rebuild_arena(gens, ngens, jobs[j].type, final_gen->ddt, entries, jobs[j].slot, new_ddt, &reclen,
&recovered, &lost);
printf(" %s: %llu custom slots recovered, %llu unrecoverable (marked not-dumped).\n",
jobs[j].type == kDataTypeCdSectorPrefix ? "prefix" : "suffix",
(unsigned long long)recovered, (unsigned long long)lost);
if(!dry_run)
{
if(append_data_block(fp, jobs[j].type, arena, reclen, templ.blockAlignmentShift,
&jobs[j].new_block_off) != 0 ||
append_ddt2(fp, &templ, jobs[j].type, new_ddt, entries, &jobs[j].new_ddt_off) != 0)
rc = EIO;
}
free(arena);
free(new_ddt);
if(rc) break;
}
// --- Rewrite the index pointing at the new blocks, and update the header ---
if(!dry_run && rc == 0)
{
for(size_t i = 0; i < index_count; i++)
{
if(index[i].blockType == DataBlock && index[i].dataType == kDataTypeCdSectorPrefix && jobs[0].new_block_off)
index[i].offset = jobs[0].new_block_off;
else if(index[i].blockType == DeDuplicationTable2 && index[i].dataType == kDataTypeCdSectorPrefix &&
jobs[0].new_ddt_off)
index[i].offset = jobs[0].new_ddt_off;
else if(index[i].blockType == DataBlock && index[i].dataType == kDataTypeCdSectorSuffix &&
jobs[1].new_block_off)
index[i].offset = jobs[1].new_block_off;
else if(index[i].blockType == DeDuplicationTable2 && index[i].dataType == kDataTypeCdSectorSuffix &&
jobs[1].new_ddt_off)
index[i].offset = jobs[1].new_ddt_off;
}
fseek(fp, 0, SEEK_END);
uint64_t new_index = (uint64_t)ftell(fp);
const uint64_t mask = (1ULL << header.blockAlignmentShift) - 1;
new_index = (new_index + mask) & ~mask;
fseek(fp, (long)new_index, SEEK_SET);
IndexHeader3 ih = {0};
ih.identifier = IndexBlock3;
ih.entries = index_count;
ih.previous = 0; // single self-contained index segment
ih.crc64 = aaruf_crc64_data((const uint8_t *)index, (uint32_t)(index_count * sizeof(IndexEntry)));
if(fwrite(&ih, sizeof(ih), 1, fp) != 1 || fwrite(index, sizeof(IndexEntry), index_count, fp) != index_count)
rc = EIO;
else
{
header.indexOffset = new_index;
fseek(fp, 0, SEEK_SET);
if(fwrite(&header, sizeof(header), 1, fp) != 1) rc = EIO;
}
if(rc == 0)
{
fflush(fp);
printf("\nRepair written. New index at %llu.\n", (unsigned long long)new_index);
}
}
else if(dry_run)
printf("\nDry run only; no changes written.\n");
for(size_t g = 0; g < ngens; g++)
{
free(gens[g].block);
free(gens[g].ddt);
}
free(gens);
free(index);
free(img);
fclose(fp);
if(rc) printf("ERROR: repair failed (%d)\n", rc);
return rc;
}

View File

@@ -46,6 +46,7 @@ void usage()
printf(" inject-media-tag Injects a media tag into an AaruFormat image.\n");
printf(" read Reads a sector and prints it out on screen.\n");
printf(" read_long Reads a sector with all its prefixes and suffixes.\n");
printf(" repair-cd-arena Repairs CD sector prefix/suffix arenas damaged by the resume bug.\n");
printf(" upgrade-ddt-to-alpha21 Upgrades a DDT image to alpha21 format.\n");
printf(" verify Verifies the integrity of blocks in an image.\n");
printf(" verify_sectors Verifies the integrity of all sectors in an image.\n\n");
@@ -162,6 +163,18 @@ void usage_upgrade_ddt_to_alpha21()
printf(" <filename> Path to the DDT image file to upgrade.\n");
}
void usage_repair_cd_arena()
{
printf("\nUsage:\n");
printf(" aaruformattool repair-cd-arena [--dry-run] <filename>\n\n");
printf("Repairs CD sector prefix/suffix arenas in a v2 image whose resumed dump sessions left the\n");
printf("final deduplication table referencing slots past the shrunken data block (garbage reads on\n");
printf("errored sectors). Recovers the slots from the orphaned dump generations still in the file.\n");
printf("Arguments:\n");
printf(" --dry-run Only report how many slots are recoverable; write nothing.\n");
printf(" <filename> Path to the AaruFormat image to repair (modified in place).\n");
}
void usage_inject_media_tag()
{
printf("\nUsage:\n");

View File

@@ -32,6 +32,7 @@ void usage_compare();
void usage_cli_compare();
void usage_convert();
void usage_upgrade_ddt_to_alpha21();
void usage_repair_cd_arena();
void usage_inject_media_tag();
void usage_convert_ps3();
void usage_convert_wiiu();