Files
Aaru/Aaru.Core/Image/Merge/Merger.cs
2026-04-12 17:50:38 +02:00

522 lines
20 KiB
C#

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Metadata;
using Aaru.Localization;
using File = System.IO.File;
using MediaType = Aaru.CommonTypes.MediaType;
using TapeFile = Aaru.CommonTypes.Structs.TapeFile;
using TapePartition = Aaru.CommonTypes.Structs.TapePartition;
namespace Aaru.Core.Image;
[SuppressMessage("Philips Naming", "PH2082:Positive Naming")]
public sealed partial class Merger
(
string primaryImagePath,
string secondaryImagePath,
string outputImagePath,
bool useSecondaryTags,
string sectorsFile,
bool ignoreMediaType,
string comments,
int count,
string creator,
string driveManufacturer,
string driveModel,
string driveFirmwareRevision,
string driveSerialNumber,
string format,
string mediaBarcode,
int lastMediaSequence,
string mediaManufacturer,
string mediaModel,
string mediaPartNumber,
int mediaSequence,
string mediaSerialNumber,
string mediaTitle,
Dictionary<string, string> options,
string primaryResumeFile,
string secondaryResumeFile,
string geometry,
bool fixSubchannelPosition,
bool fixSubchannel,
bool fixSubchannelCrc,
bool generateSubchannels,
bool decrypt,
bool ignoreNegativeSectors,
bool ignoreOverflowSectors
)
{
const string MODULE_NAME = "Image merger";
bool _aborted;
readonly PluginRegister _plugins = PluginRegister.Singleton;
byte[][]? _aacsDecryptedCpsUnitKeys;
public ErrorNumber Start()
{
// Validate sector count parameter
if(count == 0)
{
StoppingErrorMessage?.Invoke(UI.Need_to_specify_more_than_zero_sectors_to_copy_at_once);
return ErrorNumber.InvalidArgument;
}
// Parse and validate CHS geometry if specified
(bool success, uint cylinders, uint heads, uint sectors)? geometryResult = ParseGeometry(geometry);
(uint cylinders, uint heads, uint sectors)? geometryValues = null;
if(geometryResult is not null)
{
if(!geometryResult.Value.success) return ErrorNumber.InvalidArgument;
geometryValues = (geometryResult.Value.cylinders, geometryResult.Value.heads, geometryResult.Value.sectors);
}
// Load resume information from sidecar files
(bool success, Resume primaryResume) = LoadMetadata(primaryResumeFile);
if(!success) return ErrorNumber.InvalidArgument;
(success, Resume secondaryResume) = LoadMetadata(secondaryResumeFile);
if(!success) return ErrorNumber.InvalidArgument;
// Verify output file doesn't already exist
if(File.Exists(outputImagePath))
{
StoppingErrorMessage?.Invoke(UI.Output_file_already_exists);
return ErrorNumber.FileExists;
}
(ErrorNumber errno, IMediaImage primaryImage) = GetInputImage(primaryImagePath);
if(primaryImage is null) return errno;
(errno, IMediaImage secondaryImage) = GetInputImage(secondaryImagePath);
if(secondaryImage is null) return errno;
// Get media type and handle obsolete type mappings for backwards compatibility
MediaType primaryMediaType = primaryImage.Info.MediaType;
// Obsolete types
#pragma warning disable 612
primaryMediaType = primaryMediaType switch
{
MediaType.SQ1500 => MediaType.SyJet,
MediaType.Bernoulli => MediaType.Bernoulli10,
MediaType.Bernoulli2 => MediaType.BernoulliBox2_20,
_ => primaryImage.Info.MediaType
};
#pragma warning restore 612
MediaType secondaryMediaType = secondaryImage.Info.MediaType;
// Obsolete types
#pragma warning disable 612
secondaryMediaType = secondaryMediaType switch
{
MediaType.SQ1500 => MediaType.SyJet,
MediaType.Bernoulli => MediaType.Bernoulli10,
MediaType.Bernoulli2 => MediaType.BernoulliBox2_20,
_ => secondaryImage.Info.MediaType
};
#pragma warning restore 612
if(!ignoreMediaType && primaryMediaType != secondaryMediaType)
{
StoppingErrorMessage?.Invoke(UI.Images_have_different_media_types_cannot_merge);
return ErrorNumber.InvalidArgument;
}
// Discover and load output format plugin
IWritableImage outputFormat = FindOutputFormat(PluginRegister.Singleton, format, outputImagePath);
if(outputFormat == null) return ErrorNumber.FormatNotFound;
UpdateStatus?.Invoke(string.Format(UI.Output_image_format_0, outputFormat.Name));
if(primaryImage.Info.Sectors != secondaryImage.Info.Sectors)
{
StoppingErrorMessage?.Invoke(UI.Images_have_different_number_of_sectors_cannot_merge);
return ErrorNumber.InvalidArgument;
}
errno = ValidateMediaCapabilities(primaryImage, secondaryImage, outputFormat, primaryMediaType);
if(errno != ErrorNumber.NoError) return errno;
// Validate sector tags compatibility between formats
errno = ValidateSectorTags(primaryImage, outputFormat, out bool useLong);
if(errno != ErrorNumber.NoError) return errno;
// Check and setup tape image support if needed
var primaryTape = primaryImage as ITapeImage;
var secondaryTape = secondaryImage as ITapeImage;
var outputTape = outputFormat as IWritableTapeImage;
errno = ValidateTapeImage(primaryTape, secondaryTape, outputTape);
if(errno != ErrorNumber.NoError) return errno;
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Parsing_sectors_file);
(List<ulong> overrideSectorsList, List<uint> overrideNegativeSectorsList) =
ParseOverrideSectorsList(sectorsFile);
EndProgress?.Invoke();
if(overrideNegativeSectorsList.Contains(0))
{
StoppingErrorMessage?.Invoke(UI.Sectors_file_contains_invalid_sector_number_0_not_continuing);
return ErrorNumber.InvalidArgument;
}
uint nominalNegativeSectors = 0;
uint nominalOverflowSectors = 0;
if(!ignoreNegativeSectors)
{
nominalNegativeSectors = primaryImage.Info.NegativeSectors;
if(secondaryImage.Info.NegativeSectors > nominalNegativeSectors)
nominalNegativeSectors = secondaryImage.Info.NegativeSectors;
}
if(!ignoreOverflowSectors)
{
nominalOverflowSectors = primaryImage.Info.OverflowSectors;
if(secondaryImage.Info.OverflowSectors > nominalOverflowSectors)
nominalOverflowSectors = secondaryImage.Info.OverflowSectors;
}
// Check if any override sectors is bigger than the biggest overflow sector
ulong maxAllowedSector = primaryImage.Info.Sectors - 1 + nominalOverflowSectors;
if(overrideSectorsList.Count > 0 && overrideSectorsList.Max() > maxAllowedSector)
{
StoppingErrorMessage
?.Invoke(string.Format(UI
.Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_continuing,
overrideSectorsList.Max(),
maxAllowedSector));
return ErrorNumber.InvalidArgument;
}
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Calculating_sectors_to_merge);
List<ulong> sectorsToCopyFromSecondImage =
CalculateSectorsToCopy(primaryImage, secondaryImage, primaryResume, secondaryResume, overrideSectorsList);
EndProgress?.Invoke();
// Flux images might contain no decoded data, which results in a sector count of 0. We allow this if the image contains flux.
var containsFlux = primaryImage is IFluxImage || secondaryImage is IFluxImage;
if(sectorsToCopyFromSecondImage.Count == 0 && !containsFlux)
{
StoppingErrorMessage
?.Invoke(UI.No_sectors_to_merge__output_image_will_be_identical_to_primary_image_not_continuing);
return ErrorNumber.InvalidArgument;
}
errno = SetupTapeImage(primaryTape, secondaryTape, outputTape);
if(errno != ErrorNumber.NoError) return errno;
// Validate optical media capabilities (sessions, hidden tracks, etc.)
if((outputFormat as IWritableOpticalImage)?.OpticalCapabilities.HasFlag(OpticalImageCapabilities
.CanStoreSessions) !=
true &&
(primaryImage as IOpticalMediaImage)?.Sessions?.Count > 1)
{
StoppingErrorMessage?.Invoke(Localization.Core.Output_format_does_not_support_sessions);
return ErrorNumber.UnsupportedMedia;
}
// Check for hidden tracks support in optical media
if((outputFormat as IWritableOpticalImage)?.OpticalCapabilities.HasFlag(OpticalImageCapabilities
.CanStoreHiddenTracks) !=
true &&
(primaryImage as IOpticalMediaImage)?.Tracks?.Any(static t => t.Sequence == 0) == true)
{
StoppingErrorMessage?.Invoke(Localization.Core.Output_format_does_not_support_hidden_tracks);
return ErrorNumber.UnsupportedMedia;
}
// Create the output image file with appropriate settings
errno = CreateOutputImage(primaryImage,
primaryMediaType,
outputFormat,
nominalNegativeSectors,
nominalOverflowSectors);
if(errno != ErrorNumber.NoError) return errno;
// Set image metadata in the output file
errno = SetImageMetadata(primaryImage, secondaryImage, outputFormat);
if(errno != ErrorNumber.NoError) return errno;
// Prepare metadata and dump hardware information
Metadata metadata = primaryImage.AaruMetadata;
List<DumpHardware> dumpHardware =
CalculateMergedDumpHardware(primaryImage,
secondaryImage,
primaryResume,
secondaryResume,
sectorsToCopyFromSecondImage);
// Convert media tags from input to output format
errno = CopyMediaTags(primaryImage, secondaryImage, outputFormat);
if(errno != ErrorNumber.NoError) return errno;
UpdateStatus?.Invoke(string.Format(UI.Copying_0_sectors_from_primary_image, primaryImage.Info.Sectors));
// Perform the actual data conversion from input to output image
if(primaryImage is IOpticalMediaImage primaryOptical &&
secondaryImage is IOpticalMediaImage secondaryOptical &&
outputFormat is IWritableOpticalImage outputOptical &&
primaryOptical.Tracks != null)
{
errno = CopyOptical(primaryOptical, secondaryOptical, outputOptical, useLong, sectorsToCopyFromSecondImage);
if(errno != ErrorNumber.NoError) return errno;
}
else
{
if(primaryTape == null || outputTape == null || !primaryTape.IsTape)
{
(uint cylinders, uint heads, uint sectors) chs =
geometryValues != null
? (geometryValues.Value.cylinders, geometryValues.Value.heads, geometryValues.Value.sectors)
: (primaryImage.Info.Cylinders, primaryImage.Info.Heads, primaryImage.Info.SectorsPerTrack);
UpdateStatus?.Invoke(string.Format(UI.Setting_geometry_to_0_cylinders_1_heads_and_2_sectors_per_track,
chs.cylinders,
chs.heads,
chs.sectors));
if(!outputFormat.SetGeometry(chs.cylinders, chs.heads, chs.sectors))
{
ErrorMessage?.Invoke(string.Format(UI.Error_0_setting_geometry_image_may_be_incorrect_continuing,
outputFormat.ErrorMessage));
}
}
errno = CopySectorsPrimary(useLong, primaryTape?.IsTape == true, primaryImage, outputFormat);
if(errno != ErrorNumber.NoError) return errno;
errno = CopySectorsTagPrimary(useLong, primaryImage, outputFormat);
if(errno != ErrorNumber.NoError) return errno;
UpdateStatus?.Invoke(string.Format(UI.Will_copy_0_sectors_from_secondary_image,
sectorsToCopyFromSecondImage.Count));
errno = CopySectorsSecondary(useLong,
primaryTape?.IsTape == true,
primaryImage,
outputFormat,
sectorsToCopyFromSecondImage);
if(errno != ErrorNumber.NoError) return errno;
errno = CopySectorsTagSecondary(useLong, primaryImage, outputFormat, sectorsToCopyFromSecondImage);
if(errno != ErrorNumber.NoError) return errno;
if(primaryImage is IFluxImage primaryFlux && outputFormat is IWritableFluxImage outputFlux)
{
IFluxImage secondaryFlux = secondaryImage as IFluxImage;
UpdateStatus?.Invoke(secondaryFlux != null
? UI.Flux_merge_primary_then_secondary_appended
: UI.Flux_merge_primary_flux_only_secondary_not_flux_image);
errno = MergeFlux(primaryFlux, secondaryFlux, outputFlux);
if(errno != ErrorNumber.NoError) return errno;
}
if(primaryTape != null && outputTape != null && primaryTape.IsTape)
{
InitProgress?.Invoke();
var currentFile = 0;
foreach(TapeFile tapeFile in primaryTape.Files)
{
if(_aborted) break;
UpdateProgress?.Invoke(string.Format(UI.Copying_file_0_of_partition_1,
tapeFile.File,
tapeFile.Partition),
currentFile + 1,
primaryTape.Files.Count);
outputTape.AddFile(tapeFile);
currentFile++;
}
EndProgress?.Invoke();
InitProgress?.Invoke();
var currentPartition = 0;
foreach(TapePartition tapePartition in primaryTape.TapePartitions)
{
if(_aborted) break;
UpdateProgress?.Invoke(string.Format(UI.Copying_tape_partition_0, tapePartition.Number),
currentPartition + 1,
primaryTape.TapePartitions.Count);
outputTape.AddPartition(tapePartition);
currentPartition++;
}
EndProgress?.Invoke();
}
}
if(nominalNegativeSectors > 0)
{
errno = CopyNegativeSectorsPrimary(useLong,
primaryImage,
outputFormat,
nominalNegativeSectors,
overrideNegativeSectorsList);
if(errno != ErrorNumber.NoError) return errno;
if(secondaryImage.Info.NegativeSectors > 0)
CopyNegativeSectorsSecondary(useLong, secondaryImage, outputFormat, overrideNegativeSectorsList);
}
if(nominalOverflowSectors > 0)
{
var overrideOverflowSectorsList = overrideSectorsList
.Where(sector => sector >= primaryImage.Info.Sectors)
.ToList();
errno = CopyOverflowSectorsPrimary(useLong,
primaryImage,
outputFormat,
nominalOverflowSectors,
overrideOverflowSectorsList);
if(errno != ErrorNumber.NoError) return errno;
if(secondaryImage.Info.OverflowSectors > 0)
CopyOverflowSectorsSecondary(useLong, secondaryImage, outputFormat, overrideOverflowSectorsList);
}
bool ret;
if(dumpHardware != null && !_aborted)
{
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Writing_dump_hardware_list);
ret = outputFormat.SetDumpHardware(dumpHardware);
if(ret) UpdateStatus?.Invoke(UI.Written_dump_hardware_list_to_output_image);
EndProgress?.Invoke();
}
if(metadata != null && !_aborted)
{
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Writing_metadata);
ret = outputFormat.SetMetadata(metadata);
if(ret) UpdateStatus?.Invoke(UI.Written_Aaru_Metadata_to_output_image);
EndProgress?.Invoke();
}
if(_aborted)
{
UpdateStatus?.Invoke(UI.Operation_canceled_the_output_file_is_not_correct);
return ErrorNumber.Canceled;
}
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Closing_output_image);
bool closed = outputFormat.Close();
EndProgress?.Invoke();
if(!closed)
{
StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_closing_output_image_Contents_are_not_correct,
outputFormat.ErrorMessage));
return ErrorNumber.WriteError;
}
UpdateStatus?.Invoke(UI.Merge_completed_successfully);
return ErrorNumber.NoError;
}
/// <summary>Event raised when the progress bar is no longer needed</summary>
public event EndProgressHandler EndProgress;
/// <summary>Event raised when a progress bar is needed</summary>
public event InitProgressHandler InitProgress;
/// <summary>Event raised to report status updates</summary>
public event UpdateStatusHandler UpdateStatus;
/// <summary>Event raised to report a non-fatal error</summary>
public event ErrorMessageHandler ErrorMessage;
/// <summary>Event raised to report a fatal error that stops the dumping operation and should call user's attention</summary>
public event ErrorMessageHandler StoppingErrorMessage;
/// <summary>Event raised to update the values of a determinate progress bar</summary>
public event UpdateProgressHandler UpdateProgress;
/// <summary>Event raised to update the status of an indeterminate progress bar</summary>
public event PulseProgressHandler PulseProgress;
/// <summary>Event raised when the progress bar is no longer needed</summary>
public event EndProgressHandler2 EndProgress2;
/// <summary>Event raised when a progress bar is needed</summary>
public event InitProgressHandler2 InitProgress2;
/// <summary>Event raised to update the values of a determinate progress bar</summary>
public event UpdateProgressHandler2 UpdateProgress2;
public void Abort()
{
_aborted = true;
}
}