diff --git a/Aaru.Core/Aaru.Core.csproj.DotSettings b/Aaru.Core/Aaru.Core.csproj.DotSettings
index 0fa4ea42a..bcac28082 100644
--- a/Aaru.Core/Aaru.Core.csproj.DotSettings
+++ b/Aaru.Core/Aaru.Core.csproj.DotSettings
@@ -1,13 +1,10 @@
- True
- True
- True
- True
- True
\ No newline at end of file
+ xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xml:space="preserve">
+ True
+ True
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/Aaru.Core/Image/Convert/Optical.cs b/Aaru.Core/Image/Convert/Optical.cs
index 1429ddefa..802461249 100644
--- a/Aaru.Core/Image/Convert/Optical.cs
+++ b/Aaru.Core/Image/Convert/Optical.cs
@@ -25,7 +25,7 @@ public partial class Convert
return ErrorNumber.WriteError;
}
- if(_decrypt) UpdateStatus?.Invoke("Decrypting encrypted sectors.");
+ if(_decrypt) UpdateStatus?.Invoke(UI.Decrypting_encrypted_sectors);
// Convert all sectors track by track
ErrorNumber errno = ConvertOpticalSectors(inputOptical, outputOptical, useLong);
diff --git a/Aaru.Core/Image/Merge/Block.cs b/Aaru.Core/Image/Merge/Block.cs
new file mode 100644
index 000000000..0d6078b37
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Block.cs
@@ -0,0 +1,47 @@
+using System.Globalization;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ private (bool success, uint cylinders, uint heads, uint sectors)? ParseGeometry(string geometryString)
+ {
+ // Parses CHS (Cylinder/Head/Sector) geometry string in format "C/H/S" or "C-H-S"
+ // Returns tuple with success flag and parsed values, or null if not specified
+
+ if(geometryString == null) return null;
+
+ string[] geometryPieces = geometryString.Split('/');
+
+ if(geometryPieces.Length == 0) geometryPieces = geometryString.Split('-');
+
+ if(geometryPieces.Length != 3)
+ {
+ StoppingErrorMessage?.Invoke(UI.Invalid_geometry_specified);
+
+ return (false, 0, 0, 0);
+ }
+
+ if(!uint.TryParse(geometryPieces[0], CultureInfo.InvariantCulture, out uint cylinders) || cylinders == 0)
+ {
+ StoppingErrorMessage?.Invoke(UI.Invalid_number_of_cylinders_specified);
+
+ return (false, 0, 0, 0);
+ }
+
+ if(!uint.TryParse(geometryPieces[1], CultureInfo.InvariantCulture, out uint heads) || heads == 0)
+ {
+ StoppingErrorMessage?.Invoke(UI.Invalid_number_of_heads_specified);
+
+ return (false, 0, 0, 0);
+ }
+
+ if(uint.TryParse(geometryPieces[2], CultureInfo.InvariantCulture, out uint sectors) && sectors != 0)
+ return (true, cylinders, heads, sectors);
+
+ StoppingErrorMessage?.Invoke(UI.Invalid_sectors_per_track_specified);
+
+ return (false, 0, 0, 0);
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Calculator.cs b/Aaru.Core/Image/Merge/Calculator.cs
new file mode 100644
index 000000000..179a094fa
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Calculator.cs
@@ -0,0 +1,255 @@
+using System.Collections.Generic;
+using System.Linq;
+using Aaru.CommonTypes.AaruMetadata;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Metadata;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ List CalculateSectorsToCopy(IMediaImage primaryImage, IMediaImage secondaryImage, Resume primaryResume,
+ Resume secondaryResume, List overrideSectorsList)
+ {
+ List primaryTries = (primaryResume != null ? primaryResume.Tries : primaryImage.DumpHardware) ??
+ [
+ new DumpHardware
+ {
+ Extents =
+ [
+ new Extent
+ {
+ Start = 0,
+ End = primaryImage.Info.Sectors - 1
+ }
+ ]
+ }
+ ];
+
+ List secondaryTries =
+ (secondaryResume != null ? secondaryResume.Tries : secondaryImage.DumpHardware) ??
+ [
+ new DumpHardware
+ {
+ Extents =
+ [
+ new Extent
+ {
+ Start = 0,
+ End = secondaryImage.Info.Sectors - 1
+ }
+ ]
+ }
+ ];
+
+ // Get all sectors that appear in secondaryTries but not in primaryTries
+ var sectorsToCopy = new List();
+
+ // Iterate through all extents in secondaryTries
+ foreach(DumpHardware secondaryHardware in secondaryTries)
+ {
+ if(secondaryHardware?.Extents == null) continue;
+
+ foreach(Extent secondaryExtent in secondaryHardware.Extents)
+ {
+ // For each sector in this secondary extent
+ for(ulong sector = secondaryExtent.Start; sector <= secondaryExtent.End; sector++)
+ {
+ // Check if this sector appears in any primary extent
+ var foundInPrimary = false;
+
+ foreach(DumpHardware primaryHardware in primaryTries)
+ {
+ if(primaryHardware?.Extents == null) continue;
+
+ if(primaryHardware.Extents.Any(primaryExtent =>
+ sector >= primaryExtent.Start &&
+ sector <= primaryExtent.End))
+ foundInPrimary = true;
+
+ if(foundInPrimary) break;
+ }
+
+ // If not found in primary, add to result
+ if(!foundInPrimary) sectorsToCopy.Add(sector);
+ }
+ }
+ }
+
+ sectorsToCopy.AddRange(overrideSectorsList.Where(t => !sectorsToCopy.Contains(t)));
+
+ return sectorsToCopy;
+ }
+
+ List CalculateMergedDumpHardware(IMediaImage primaryImage, IMediaImage secondaryImage,
+ Resume primaryResume, Resume secondaryResume,
+ List overrideSectorsList)
+ {
+ List primaryTries = (primaryResume != null ? primaryResume.Tries : primaryImage.DumpHardware) ??
+ [
+ new DumpHardware
+ {
+ Extents =
+ [
+ new Extent
+ {
+ Start = 0,
+ End = primaryImage.Info.Sectors - 1
+ }
+ ]
+ }
+ ];
+
+ List secondaryTries =
+ (secondaryResume != null ? secondaryResume.Tries : secondaryImage.DumpHardware) ??
+ [
+ new DumpHardware
+ {
+ Extents =
+ [
+ new Extent
+ {
+ Start = 0,
+ End = secondaryImage.Info.Sectors - 1
+ }
+ ]
+ }
+ ];
+
+ var mergedHardware = new List();
+
+ // Create a mapping of which hardware each sector belongs to
+ var sectorToHardware = new Dictionary();
+
+ // First, build a lookup of which hardware each sector belongs to in primary tries
+ var primarySectorToHardware = new Dictionary();
+
+ foreach(DumpHardware primaryHardware in primaryTries)
+ {
+ if(primaryHardware?.Extents == null) continue;
+
+ foreach(Extent extent in primaryHardware.Extents)
+ {
+ for(ulong sector = extent.Start; sector <= extent.End; sector++)
+ primarySectorToHardware[sector] = primaryHardware;
+ }
+ }
+
+ // Build a lookup of which hardware each sector belongs to in secondary tries
+ var secondarySectorToHardware = new Dictionary();
+
+ foreach(DumpHardware secondaryHardware in secondaryTries)
+ {
+ if(secondaryHardware?.Extents == null) continue;
+
+ foreach(Extent extent in secondaryHardware.Extents)
+ {
+ for(ulong sector = extent.Start; sector <= extent.End; sector++)
+ secondarySectorToHardware[sector] = secondaryHardware;
+ }
+ }
+
+ // Now assign hardware to each sector: use primary hardware, unless sector is in override list
+ foreach((ulong sector, DumpHardware primaryHardware) in primarySectorToHardware)
+ {
+ // If this sector should be overridden, use secondary hardware instead
+ if(overrideSectorsList.Contains(sector))
+ {
+ if(secondarySectorToHardware.TryGetValue(sector, out DumpHardware secondaryHardware))
+ sectorToHardware[sector] = secondaryHardware;
+ }
+ else
+ {
+ // Use primary hardware
+ sectorToHardware[sector] = primaryHardware;
+ }
+ }
+
+ // Also add any sectors from override list that weren't in primary
+ foreach(ulong overrideSector in overrideSectorsList)
+ {
+ if(!sectorToHardware.ContainsKey(overrideSector) &&
+ secondarySectorToHardware.TryGetValue(overrideSector, out DumpHardware secondaryHardware))
+ sectorToHardware[overrideSector] = secondaryHardware;
+ }
+
+ // Create extents preserving sector order, grouping contiguous sectors from same hardware
+ var allSectors = sectorToHardware.Keys.Order().ToList();
+
+ if(allSectors.Count == 0) return mergedHardware;
+
+ // Start first extent
+ DumpHardware currentHardware = sectorToHardware[allSectors[0]];
+ ulong extentStart = allSectors[0];
+ ulong extentEnd = allSectors[0];
+
+ for(var i = 1; i < allSectors.Count; i++)
+ {
+ ulong sector = allSectors[i];
+ DumpHardware hw = sectorToHardware[sector];
+
+ // Check if we should continue current extent or start new one
+ if(hw == currentHardware && sector == extentEnd + 1)
+ {
+ // Same hardware and contiguous sector, extend current extent
+ extentEnd = sector;
+ }
+ else
+ {
+ // Hardware changed or gap in sectors, save current extent and start new one
+ AddOrUpdateHardware(mergedHardware, currentHardware, extentStart, extentEnd);
+
+ currentHardware = hw;
+ extentStart = sector;
+ extentEnd = sector;
+ }
+ }
+
+ // Add the last extent
+ AddOrUpdateHardware(mergedHardware, currentHardware, extentStart, extentEnd);
+
+ return mergedHardware;
+ }
+
+ static void AddOrUpdateHardware(List mergedHardware, DumpHardware originalHardware, ulong start,
+ ulong end)
+ {
+ // Check if we already have an entry for this hardware
+ DumpHardware existing = mergedHardware.FirstOrDefault(h => h.Manufacturer == originalHardware.Manufacturer &&
+ h.Model == originalHardware.Model &&
+ h.Revision == originalHardware.Revision &&
+ h.Firmware == originalHardware.Firmware &&
+ h.Serial == originalHardware.Serial);
+
+ if(existing != null)
+ {
+ // Add extent to existing hardware
+ existing.Extents.Add(new Extent
+ {
+ Start = start,
+ End = end
+ });
+ }
+ else
+ {
+ // Create new hardware entry
+ mergedHardware.Add(new DumpHardware
+ {
+ Manufacturer = originalHardware.Manufacturer,
+ Model = originalHardware.Model,
+ Revision = originalHardware.Revision,
+ Firmware = originalHardware.Firmware,
+ Serial = originalHardware.Serial,
+ Software = originalHardware.Software,
+ Extents =
+ [
+ new Extent
+ {
+ Start = start,
+ End = end
+ }
+ ]
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Capabilities.cs b/Aaru.Core/Image/Merge/Capabilities.cs
new file mode 100644
index 000000000..fdd88bb6b
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Capabilities.cs
@@ -0,0 +1,70 @@
+using System.Linq;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber ValidateMediaCapabilities(IMediaImage primaryImage, IMediaImage secondaryImage,
+ IWritableImage outputImage, MediaType mediaType)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ if(!outputImage.SupportedMediaTypes.Contains(mediaType))
+ {
+ StoppingErrorMessage?.Invoke(UI.Output_format_does_not_support_media_type);
+
+ return ErrorNumber.UnsupportedMedia;
+ }
+
+ foreach(MediaTagType mediaTag in primaryImage.Info.ReadableMediaTags
+ .Where(mediaTag =>
+ !outputImage.SupportedMediaTags.Contains(mediaTag))
+ .TakeWhile(_ => !_aborted))
+ {
+ StoppingErrorMessage
+ ?.Invoke(string.Format(UI.Media_tag_0_present_in_primary_image_will_be_lost_in_output_format, mediaTag));
+
+ return ErrorNumber.DataWillBeLost;
+ }
+
+ foreach(MediaTagType mediaTag in secondaryImage.Info.ReadableMediaTags
+ .Where(mediaTag =>
+ !primaryImage.Info.ReadableMediaTags
+ .Contains(mediaTag) &&
+ !outputImage.SupportedMediaTags.Contains(mediaTag))
+ .TakeWhile(_ => !_aborted))
+ {
+ StoppingErrorMessage
+ ?.Invoke(string.Format(UI.Media_tag_0_present_in_secondary_image_will_be_lost_in_output_format,
+ mediaTag));
+
+ return ErrorNumber.DataWillBeLost;
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber ValidateSectorTags(IMediaImage primaryImage, IWritableImage outputImage, out bool useLong)
+ {
+ useLong = primaryImage.Info.ReadableSectorTags.Count != 0;
+
+ if(_aborted) return ErrorNumber.NoError;
+
+ foreach(SectorTagType sectorTag in primaryImage.Info.ReadableSectorTags
+ .Where(sectorTag =>
+ !outputImage.SupportedSectorTags.Contains(sectorTag))
+ .TakeWhile(_ => !_aborted))
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Output_image_does_not_support_sector_tag_0_data_will_be_lost,
+ sectorTag));
+
+ return ErrorNumber.DataWillBeLost;
+ }
+
+ return ErrorNumber.NoError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Create.cs b/Aaru.Core/Image/Merge/Create.cs
new file mode 100644
index 000000000..87bd81347
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Create.cs
@@ -0,0 +1,34 @@
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ private ErrorNumber CreateOutputImage(IMediaImage primaryImage, MediaType mediaType, IWritableImage outputImage,
+ uint negativeSectors, uint overflowSectors)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+ PulseProgress?.Invoke(UI.Invoke_Opening_image_file);
+
+ bool created = outputImage.Create(outputImagePath,
+ mediaType,
+ options,
+ primaryImage.Info.Sectors,
+ negativeSectors,
+ overflowSectors,
+ primaryImage.Info.SectorSize);
+
+ EndProgress?.Invoke();
+
+ if(created) return ErrorNumber.NoError;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_creating_output_image, outputImage.ErrorMessage));
+
+ return ErrorNumber.CannotCreateFormat;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Edge.cs b/Aaru.Core/Image/Merge/Edge.cs
new file mode 100644
index 000000000..1d8b4a7bc
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Edge.cs
@@ -0,0 +1,632 @@
+using System.Collections.Generic;
+using System.Linq;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber CopyNegativeSectorsPrimary(bool useLong, IMediaImage primaryImage, IWritableImage outputImage,
+ uint negativeSectors, List overrideNegativeSectors)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ ErrorNumber errno = ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+
+ List notDumped = [];
+
+ // There's no -0
+ for(uint i = 1; i <= negativeSectors; i++)
+ {
+ if(_aborted) break;
+
+ byte[] sector;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_negative_sector_0_of_1, i, negativeSectors),
+ i,
+ negativeSectors);
+
+ bool result;
+ SectorStatus sectorStatus;
+
+ if(useLong)
+ {
+ errno = primaryImage.ReadSectorLong(i, true, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(i);
+
+ continue;
+ }
+
+ result = outputImage.WriteSectorLong(sector, i, true, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ i));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = primaryImage.ReadSector(i, true, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(i);
+
+ continue;
+ }
+
+ result = outputImage.WriteSector(sector, i, true, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ i));
+
+ return errno;
+ }
+ }
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_negative_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ i));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+
+ foreach(SectorTagType tag in primaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ case SectorTagType.CdTrackFlags:
+ case SectorTagType.CdTrackIsrc:
+ case SectorTagType.CdTrackText:
+ // These tags are track tags
+ continue;
+ }
+
+ InitProgress?.Invoke();
+
+ for(uint i = 1; i <= negativeSectors; i++)
+ {
+ if(_aborted) break;
+
+ if(notDumped.Contains(i)) continue;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_1_for_negative_sector_0, i, tag),
+ i,
+ negativeSectors);
+
+ bool result;
+
+ errno = primaryImage.ReadSectorTag(i, true, tag, out byte[] sector);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorTag(sector, i, true, tag);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ i));
+
+ return errno;
+ }
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_negative_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ i));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ overrideNegativeSectors.AddRange(notDumped.Where(t => !overrideNegativeSectors.Contains(t)));
+ overrideNegativeSectors.Sort();
+
+ return errno;
+ }
+
+ ErrorNumber CopyNegativeSectorsSecondary(bool useLong, IMediaImage secondaryImage, IWritableImage outputImage,
+ List overrideNegativeSectors)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ ErrorNumber errno = ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+
+ List notDumped = [];
+ var currentCount = 0;
+ int totalCount = overrideNegativeSectors.Count;
+
+ foreach(uint sectorAddress in overrideNegativeSectors)
+ {
+ if(_aborted) break;
+
+ byte[] sector;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_negative_sector_0, sectorAddress),
+ currentCount,
+ totalCount);
+
+ bool result;
+ SectorStatus sectorStatus;
+
+ if(useLong)
+ {
+ errno = secondaryImage.ReadSectorLong(sectorAddress, true, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(sectorAddress);
+
+ continue;
+ }
+
+ result = outputImage.WriteSectorLong(sector, sectorAddress, true, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = secondaryImage.ReadSector(sectorAddress, true, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(sectorAddress);
+
+ continue;
+ }
+
+ result = outputImage.WriteSector(sector, sectorAddress, true, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+ }
+
+ currentCount++;
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_negative_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+
+ foreach(SectorTagType tag in secondaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ case SectorTagType.CdTrackFlags:
+ case SectorTagType.CdTrackIsrc:
+ case SectorTagType.CdTrackText:
+ // These tags are track tags
+ continue;
+ }
+
+ InitProgress?.Invoke();
+
+ currentCount = 0;
+
+ foreach(uint sectorAddress in overrideNegativeSectors)
+ {
+ if(_aborted) break;
+
+ if(notDumped.Contains(sectorAddress)) continue;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_1_for_negative_sector_0, sectorAddress, tag),
+ currentCount,
+ totalCount);
+
+ bool result;
+
+ errno = secondaryImage.ReadSectorTag(sectorAddress, true, tag, out byte[] sector);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorTag(sector, sectorAddress, true, tag);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_negative_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+
+ currentCount++;
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_negative_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ return errno;
+ }
+
+ ErrorNumber CopyOverflowSectorsPrimary(bool useLong, IMediaImage primaryImage, IWritableImage outputImage,
+ uint overflowSectors, List overrideOverflowSectors)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ ErrorNumber errno = ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+
+ List notDumped = [];
+
+ for(uint i = 0; i < overflowSectors; i++)
+ {
+ if(_aborted) break;
+
+ byte[] sector;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_overflow_sector_0_of_1, i, overflowSectors),
+ i,
+ overflowSectors);
+
+ bool result;
+ SectorStatus sectorStatus;
+
+ if(useLong)
+ {
+ errno = primaryImage.ReadSectorLong(primaryImage.Info.Sectors + i, false, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(i);
+
+ continue;
+ }
+
+ result = outputImage.WriteSectorLong(sector, primaryImage.Info.Sectors + i, false, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ i));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = primaryImage.ReadSector(primaryImage.Info.Sectors + i, false, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(i);
+
+ continue;
+ }
+
+ result = outputImage.WriteSector(sector, primaryImage.Info.Sectors + i, false, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ i));
+
+ return errno;
+ }
+ }
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_overflow_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ i));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+
+ foreach(SectorTagType tag in primaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ case SectorTagType.CdTrackFlags:
+ case SectorTagType.CdTrackIsrc:
+ case SectorTagType.CdTrackText:
+ // These tags are track tags
+ continue;
+ }
+
+ InitProgress?.Invoke();
+
+ for(uint i = 1; i < overflowSectors; i++)
+ {
+ if(_aborted) break;
+
+ if(notDumped.Contains(i)) continue;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_1_for_overflow_sector_0, i, tag),
+ i,
+ overflowSectors);
+
+ bool result;
+
+ errno = primaryImage.ReadSectorTag(primaryImage.Info.Sectors + i, false, tag, out byte[] sector);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorTag(sector, primaryImage.Info.Sectors + i, false, tag);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ primaryImage.Info.Sectors + i));
+
+ return errno;
+ }
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_overflow_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ primaryImage.Info.Sectors + i));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+ }
+
+ foreach(uint sector in notDumped.Where(t => !overrideOverflowSectors.Contains(primaryImage.Info.Sectors + t)))
+ overrideOverflowSectors.Add(primaryImage.Info.Sectors + sector);
+
+ overrideOverflowSectors.Sort();
+
+ return errno;
+ }
+
+ ErrorNumber CopyOverflowSectorsSecondary(bool useLong, IMediaImage secondaryImage, IWritableImage outputImage,
+ List overrideOverflowSectors)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ ErrorNumber errno = ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+
+ List notDumped = [];
+
+ int overflowSectors = overrideOverflowSectors.Count;
+ var currentSector = 0;
+
+ foreach(ulong sectorAddress in overrideOverflowSectors)
+ {
+ if(_aborted) break;
+
+ byte[] sector;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_overflow_sector_0, sectorAddress),
+ currentSector,
+ overflowSectors);
+
+ bool result;
+ SectorStatus sectorStatus;
+
+ if(useLong)
+ {
+ errno = secondaryImage.ReadSectorLong(sectorAddress, false, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(sectorAddress);
+
+ continue;
+ }
+
+ result = outputImage.WriteSectorLong(sector, sectorAddress, false, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = secondaryImage.ReadSector(sectorAddress, false, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(sectorStatus == SectorStatus.NotDumped)
+ {
+ notDumped.Add(sectorAddress);
+
+ continue;
+ }
+
+ result = outputImage.WriteSector(sector, sectorAddress, false, sectorStatus);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+ }
+
+ currentSector++;
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_overflow_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+
+ foreach(SectorTagType tag in secondaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ case SectorTagType.CdTrackFlags:
+ case SectorTagType.CdTrackIsrc:
+ case SectorTagType.CdTrackText:
+ // These tags are track tags
+ continue;
+ }
+
+ InitProgress?.Invoke();
+
+ currentSector = 0;
+
+ foreach(ulong sectorAddress in overrideOverflowSectors)
+ {
+ if(_aborted) break;
+
+ if(notDumped.Contains(sectorAddress)) continue;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_1_for_overflow_sector_0, sectorAddress, tag),
+ currentSector,
+ overflowSectors);
+
+ bool result;
+
+ errno = secondaryImage.ReadSectorTag(sectorAddress, false, tag, out byte[] sector);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorTag(sector, sectorAddress, false, tag);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_overflow_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+
+ currentSector++;
+
+ if(result) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_overflow_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+ }
+
+ return errno;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Flux.cs b/Aaru.Core/Image/Merge/Flux.cs
new file mode 100644
index 000000000..d3fbd189d
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Flux.cs
@@ -0,0 +1,52 @@
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ // TODO: Should we return error any time?
+ // TODO: Add progress reporting
+ ErrorNumber CopyFlux(IFluxImage inputFlux, IWritableFluxImage outputFlux)
+ {
+ for(ushort track = 0; track < inputFlux.Info.Cylinders; track++)
+ {
+ for(uint head = 0; head < inputFlux.Info.Heads; head++)
+ {
+ ErrorNumber error = inputFlux.SubTrackLength(head, track, out byte subTrackLen);
+
+ if(error != ErrorNumber.NoError) continue;
+
+ for(byte subTrackIndex = 0; subTrackIndex < subTrackLen; subTrackIndex++)
+ {
+ error = inputFlux.CapturesLength(head, track, subTrackIndex, out uint capturesLen);
+
+ if(error != ErrorNumber.NoError) continue;
+
+ for(uint captureIndex = 0; captureIndex < capturesLen; captureIndex++)
+ {
+ inputFlux.ReadFluxCapture(head,
+ track,
+ subTrackIndex,
+ captureIndex,
+ out ulong indexResolution,
+ out ulong dataResolution,
+ out byte[] indexBuffer,
+ out byte[] dataBuffer);
+
+ outputFlux.WriteFluxCapture(indexResolution,
+ dataResolution,
+ indexBuffer,
+ dataBuffer,
+ head,
+ track,
+ subTrackIndex,
+ captureIndex);
+ }
+ }
+ }
+ }
+
+ return ErrorNumber.NoError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Format.cs b/Aaru.Core/Image/Merge/Format.cs
new file mode 100644
index 000000000..9ce90da23
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Format.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+using Aaru.Logging;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ (ErrorNumber error, IMediaImage inputFormat) GetInputImage(string imagePath)
+ {
+ // Identify input file filter (determines file type handler)
+
+ InitProgress?.Invoke();
+
+ PulseProgress?.Invoke(UI.Identifying_file_filter);
+
+ IFilter inputFilter = PluginRegister.Singleton.GetFilter(imagePath);
+
+ EndProgress?.Invoke();
+
+ if(inputFilter == null)
+ {
+ AaruLogging.Error(UI.Cannot_open_specified_file);
+
+ return (ErrorNumber.CannotOpenFile, null);
+ }
+
+ // Identify input image format
+
+ InitProgress?.Invoke();
+ PulseProgress?.Invoke(UI.Identifying_image_format);
+
+ IBaseImage baseImage = ImageFormat.Detect(inputFilter);
+ var inputFormat = baseImage as IMediaImage;
+
+ EndProgress?.Invoke();
+
+
+ if(baseImage == null)
+ {
+ StoppingErrorMessage?.Invoke(UI.Input_image_format_not_identified);
+
+ return (ErrorNumber.UnrecognizedFormat, null);
+ }
+
+ if(inputFormat == null)
+ {
+ StoppingErrorMessage?.Invoke(UI.Command_not_yet_supported_for_this_image_type);
+
+ return (ErrorNumber.InvalidArgument, null);
+ }
+
+ UpdateStatus?.Invoke(string.Format(UI.Input_image_format_identified_by_0, inputFormat.Name));
+
+ try
+ {
+ // Open the input image file for reading
+
+ InitProgress?.Invoke();
+ PulseProgress?.Invoke(UI.Invoke_Opening_image_file);
+
+ ErrorNumber opened = inputFormat.Open(inputFilter);
+
+ EndProgress?.Invoke();
+
+ if(opened != ErrorNumber.NoError)
+ {
+ StoppingErrorMessage?.Invoke(UI.Unable_to_open_image_format +
+ Environment.NewLine +
+ string.Format(Localization.Core.Error_0, opened));
+
+ return (opened, null);
+ }
+
+ // Get media type and handle obsolete type mappings for backwards compatibility
+ MediaType mediaType = inputFormat.Info.MediaType;
+
+ // Obsolete types
+#pragma warning disable 612
+ mediaType = mediaType switch
+ {
+ MediaType.SQ1500 => MediaType.SyJet,
+ MediaType.Bernoulli => MediaType.Bernoulli10,
+ MediaType.Bernoulli2 => MediaType.BernoulliBox2_20,
+ _ => inputFormat.Info.MediaType
+ };
+#pragma warning restore 612
+
+ AaruLogging.Debug(MODULE_NAME, UI.Correctly_opened_image_file);
+
+ // Log image statistics for debugging
+ AaruLogging.Debug(MODULE_NAME, UI.Image_without_headers_is_0_bytes, inputFormat.Info.ImageSize);
+
+ AaruLogging.Debug(MODULE_NAME, UI.Image_has_0_sectors, inputFormat.Info.Sectors);
+
+ AaruLogging.Debug(MODULE_NAME, UI.Image_identifies_media_type_as_0, mediaType);
+
+ Statistics.AddMediaFormat(inputFormat.Format);
+ Statistics.AddMedia(mediaType, false);
+ Statistics.AddFilter(inputFilter.Name);
+
+ return (ErrorNumber.NoError, inputFormat);
+ }
+ catch(Exception ex)
+ {
+ StoppingErrorMessage?.Invoke(UI.Unable_to_open_image_format +
+ Environment.NewLine +
+ string.Format(Localization.Core.Error_0, ex.Message));
+
+ AaruLogging.Exception(ex, Localization.Core.Error_0, ex.Message);
+
+ return (ErrorNumber.CannotOpenFormat, null);
+ }
+ }
+
+ private IWritableImage FindOutputFormat(PluginRegister plugins, string format, string outputPath)
+ {
+ // Discovers output format plugin by extension, GUID, or name
+ // Searches writable format plugins matching any of three methods:
+ // 1. By file extension (if format not specified)
+ // 2. By plugin GUID (if format is valid GUID)
+ // 3. By plugin name (case-insensitive string match)
+ // Returns null if no match or multiple matches found
+
+ List candidates = [];
+
+ // Try extension
+ if(string.IsNullOrEmpty(format))
+ {
+ candidates.AddRange(from plugin in plugins.WritableImages.Values
+ where plugin is not null
+ where plugin.KnownExtensions.Contains(Path.GetExtension(outputPath))
+ select plugin);
+ }
+
+ // Try Id
+ else if(Guid.TryParse(format, CultureInfo.CurrentCulture, out Guid outId))
+ {
+ candidates.AddRange(from plugin in plugins.WritableImages.Values
+ where plugin is not null
+ where plugin.Id == outId
+ select plugin);
+ }
+
+ // Try name
+ else
+ {
+ candidates.AddRange(from plugin in plugins.WritableImages.Values
+ where plugin is not null
+ where plugin.Name.Equals(format, StringComparison.InvariantCultureIgnoreCase)
+ select plugin);
+ }
+
+ switch(candidates.Count)
+ {
+ case 0:
+ StoppingErrorMessage?.Invoke(UI.No_plugin_supports_requested_extension);
+
+ return null;
+ case > 1:
+ StoppingErrorMessage?.Invoke(UI.More_than_one_plugin_supports_requested_extension);
+
+ return null;
+ }
+
+ return candidates[0] as IWritableImage;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Merger.cs b/Aaru.Core/Image/Merge/Merger.cs
new file mode 100644
index 000000000..3a326612c
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Merger.cs
@@ -0,0 +1,531 @@
+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 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;
+
+ 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 overrideSectorsList, List 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 sectorsToCopyFromSecondImage =
+ CalculateSectorsToCopy(primaryImage, secondaryImage, primaryResume, secondaryResume, overrideSectorsList);
+
+ EndProgress?.Invoke();
+
+ if(sectorsToCopyFromSecondImage.Count == 0)
+ {
+ 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)
+ {
+ // TODO: Disabled until 6.0
+ /*if(!_force)
+ {*/
+ StoppingErrorMessage?.Invoke(Localization.Core.Output_format_does_not_support_sessions);
+
+ return ErrorNumber.UnsupportedMedia;
+ /*}
+
+ StoppingErrorMessage?.Invoke("Output format does not support sessions, this will end in a loss of data, continuing...");*/
+ }
+
+ // 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)
+ {
+ // TODO: Disabled until 6.0
+ /*if(!_force)
+ {*/
+ StoppingErrorMessage?.Invoke(Localization.Core.Output_format_does_not_support_hidden_tracks);
+
+ return ErrorNumber.UnsupportedMedia;
+ /*}
+
+ StoppingErrorMessage?.Invoke("Output format does not support sessions, this will end in a loss of data, continuing...");*/
+ }
+
+ // 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;
+ InitProgress?.Invoke();
+ PulseProgress?.Invoke(UI.Calculating_merged_dump_hardware_list);
+
+ List dumpHardware =
+ CalculateMergedDumpHardware(primaryImage,
+ secondaryImage,
+ primaryResume,
+ secondaryResume,
+ sectorsToCopyFromSecondImage);
+
+ EndProgress?.Invoke();
+
+ // 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 inputFlux && outputFormat is IWritableFluxImage outputFlux)
+ {
+ UpdateStatus?.Invoke(UI.Flux_data_will_be_copied_as_is_from_primary_image);
+ errno = CopyFlux(inputFlux, 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;
+
+ errno = CopyNegativeSectorsSecondary(useLong, secondaryImage, outputFormat, overrideNegativeSectorsList);
+
+ if(errno != ErrorNumber.NoError) return errno;
+ }
+
+ 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;
+
+ errno = CopyOverflowSectorsSecondary(useLong, secondaryImage, outputFormat, overrideOverflowSectorsList);
+
+ if(errno != ErrorNumber.NoError) return errno;
+ }
+
+ 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;
+ }
+
+ /// Event raised when the progress bar is no longer needed
+ public event EndProgressHandler EndProgress;
+
+ /// Event raised when a progress bar is needed
+ public event InitProgressHandler InitProgress;
+
+ /// Event raised to report status updates
+ public event UpdateStatusHandler UpdateStatus;
+
+ /// Event raised to report a non-fatal error
+ public event ErrorMessageHandler ErrorMessage;
+
+ /// Event raised to report a fatal error that stops the dumping operation and should call user's attention
+ public event ErrorMessageHandler StoppingErrorMessage;
+
+ /// Event raised to update the values of a determinate progress bar
+ public event UpdateProgressHandler UpdateProgress;
+
+ /// Event raised to update the status of an indeterminate progress bar
+ public event PulseProgressHandler PulseProgress;
+
+ /// Event raised when the progress bar is no longer needed
+ public event EndProgressHandler2 EndProgress2;
+
+ /// Event raised when a progress bar is needed
+ public event InitProgressHandler2 InitProgress2;
+
+ /// Event raised to update the values of a determinate progress bar
+ public event UpdateProgressHandler2 UpdateProgress2;
+
+ public void Abort()
+ {
+ _aborted = true;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Metadata.cs b/Aaru.Core/Image/Merge/Metadata.cs
new file mode 100644
index 000000000..efa8fe7e5
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Metadata.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Xml.Serialization;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Metadata;
+using Aaru.Localization;
+using Aaru.Logging;
+using Version = Aaru.CommonTypes.Interop.Version;
+
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ private (bool success, Resume resume) LoadMetadata(string resumeFilePath)
+ {
+ Resume resume;
+
+ if(resumeFilePath == null) return (true, null);
+
+ if(File.Exists(resumeFilePath))
+ {
+ try
+ {
+ if(resumeFilePath.EndsWith(".resume.json", StringComparison.CurrentCultureIgnoreCase))
+ {
+ var fs = new FileStream(resumeFilePath, FileMode.Open);
+
+ resume =
+ (JsonSerializer.Deserialize(fs, typeof(ResumeJson), ResumeJsonContext.Default) as ResumeJson)
+ ?.Resume;
+
+ fs.Close();
+ }
+ else
+ {
+ // Bypassed by JSON source generator used above
+#pragma warning disable IL2026
+ var xs = new XmlSerializer(typeof(Resume));
+#pragma warning restore IL2026
+
+ var sr = new StreamReader(resumeFilePath);
+
+ // Bypassed by JSON source generator used above
+#pragma warning disable IL2026
+ resume = (Resume)xs.Deserialize(sr);
+#pragma warning restore IL2026
+
+ sr.Close();
+ }
+ }
+ catch(Exception ex)
+ {
+ StoppingErrorMessage?.Invoke(UI.Incorrect_resume_file_not_continuing);
+ AaruLogging.Exception(ex, UI.Incorrect_resume_file_not_continuing);
+
+ return (false, null);
+ }
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(UI.Could_not_find_resume_file);
+
+ return (false, null);
+ }
+
+ return (true, resume);
+ }
+
+ ErrorNumber SetImageMetadata(IMediaImage primaryImage, IMediaImage secondaryImage, IWritableImage outputImage)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ var imageInfo = new CommonTypes.Structs.ImageInfo
+ {
+ Application = "Aaru",
+ ApplicationVersion = Version.GetInformationalVersion(),
+ Comments = comments ?? primaryImage.Info.Comments ?? secondaryImage.Info.Comments,
+ Creator = creator ?? primaryImage.Info.Creator ?? secondaryImage.Info.Creator,
+ DriveFirmwareRevision =
+ driveFirmwareRevision ??
+ primaryImage.Info.DriveFirmwareRevision ?? secondaryImage.Info.DriveFirmwareRevision,
+ DriveManufacturer =
+ driveManufacturer ?? primaryImage.Info.DriveManufacturer ?? secondaryImage.Info.DriveManufacturer,
+ DriveModel = driveModel ?? primaryImage.Info.DriveModel ?? secondaryImage.Info.DriveModel,
+ DriveSerialNumber =
+ driveSerialNumber ?? primaryImage.Info.DriveSerialNumber ?? secondaryImage.Info.DriveSerialNumber,
+ LastMediaSequence = lastMediaSequence != 0
+ ? lastMediaSequence
+ : primaryImage.Info.LastMediaSequence != 0
+ ? primaryImage.Info.LastMediaSequence
+ : secondaryImage.Info.LastMediaSequence,
+ MediaBarcode = mediaBarcode ?? primaryImage.Info.MediaBarcode ?? secondaryImage.Info.MediaBarcode,
+ MediaManufacturer =
+ mediaManufacturer ?? primaryImage.Info.MediaManufacturer ?? secondaryImage.Info.MediaManufacturer,
+ MediaModel = mediaModel ?? primaryImage.Info.MediaModel ?? secondaryImage.Info.MediaModel,
+ MediaPartNumber =
+ mediaPartNumber ?? primaryImage.Info.MediaPartNumber ?? secondaryImage.Info.MediaPartNumber,
+ MediaSequence = mediaSequence != 0
+ ? mediaSequence
+ : primaryImage.Info.MediaSequence != 0
+ ? primaryImage.Info.MediaSequence
+ : secondaryImage.Info.MediaSequence,
+ MediaSerialNumber =
+ mediaSerialNumber ?? primaryImage.Info.MediaSerialNumber ?? secondaryImage.Info.MediaSerialNumber,
+ MediaTitle = mediaTitle ?? primaryImage.Info.MediaTitle ?? secondaryImage.Info.MediaTitle
+ };
+
+ if(outputImage.SetImageInfo(imageInfo)) return ErrorNumber.NoError;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_setting_metadata_not_continuing,
+ outputImage.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Optical.cs b/Aaru.Core/Image/Merge/Optical.cs
new file mode 100644
index 000000000..86c368b4e
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Optical.cs
@@ -0,0 +1,891 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+using Aaru.Core.Media;
+using Aaru.Decryption.DVD;
+using Aaru.Devices;
+using Aaru.Localization;
+using Aaru.Logging;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber CopyOptical(IOpticalMediaImage primaryOptical, IOpticalMediaImage secondaryOptical,
+ IWritableOpticalImage outputOptical, bool useLong, List sectorsToCopyFromSecondImage)
+ {
+ if(!outputOptical.SetTracks(primaryOptical.Tracks))
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_sending_tracks_list_to_output_image,
+ outputOptical.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+
+ if(decrypt) UpdateStatus?.Invoke(UI.Decrypting_encrypted_sectors);
+
+ // Convert all sectors track by track
+ ErrorNumber errno = CopyOpticalSectorsPrimary(primaryOptical, outputOptical, useLong);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ Dictionary isrcs = [];
+ Dictionary trackFlags = [];
+ string mcn = null;
+ HashSet subchannelExtents = [];
+ Dictionary smallestPregapLbaPerTrack = [];
+ var tracks = new Track[primaryOptical.Tracks.Count];
+
+ for(var i = 0; i < tracks.Length; i++)
+ {
+ tracks[i] = new Track
+ {
+ Indexes = [],
+ Description = primaryOptical.Tracks[i].Description,
+ EndSector = primaryOptical.Tracks[i].EndSector,
+ StartSector = primaryOptical.Tracks[i].StartSector,
+ Pregap = primaryOptical.Tracks[i].Pregap,
+ Sequence = primaryOptical.Tracks[i].Sequence,
+ Session = primaryOptical.Tracks[i].Session,
+ BytesPerSector = primaryOptical.Tracks[i].BytesPerSector,
+ RawBytesPerSector = primaryOptical.Tracks[i].RawBytesPerSector,
+ Type = primaryOptical.Tracks[i].Type,
+ SubchannelType = primaryOptical.Tracks[i].SubchannelType
+ };
+
+ foreach(KeyValuePair idx in primaryOptical.Tracks[i].Indexes)
+ tracks[i].Indexes[idx.Key] = idx.Value;
+ }
+
+ // Gets tracks ISRCs
+ foreach(SectorTagType tag in primaryOptical.Info.ReadableSectorTags
+ .Where(static t => t == SectorTagType.CdTrackIsrc)
+ .Order())
+ {
+ foreach(Track track in tracks)
+ {
+ errno = primaryOptical.ReadSectorTag(track.Sequence, false, tag, out byte[] isrc);
+
+ if(errno != ErrorNumber.NoError) continue;
+
+ isrcs[(byte)track.Sequence] = Encoding.UTF8.GetString(isrc);
+ }
+ }
+
+ // Gets tracks flags
+ foreach(SectorTagType tag in primaryOptical.Info.ReadableSectorTags
+ .Where(static t => t == SectorTagType.CdTrackFlags)
+ .Order())
+ {
+ foreach(Track track in tracks)
+ {
+ errno = primaryOptical.ReadSectorTag(track.Sequence, false, tag, out byte[] flags);
+
+ if(errno != ErrorNumber.NoError) continue;
+
+ trackFlags[(byte)track.Sequence] = flags[0];
+ }
+ }
+
+ // Gets subchannel extents
+ for(ulong s = 0; s < primaryOptical.Info.Sectors; s++)
+ {
+ if(s > int.MaxValue) break;
+
+ subchannelExtents.Add((int)s);
+ }
+
+ errno = CopyOpticalSectorsTagsPrimary(primaryOptical,
+ outputOptical,
+ useLong,
+ isrcs,
+ ref mcn,
+ tracks,
+ subchannelExtents,
+ smallestPregapLbaPerTrack);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ // Write ISRCs
+ foreach(KeyValuePair isrc in isrcs)
+ {
+ outputOptical.WriteSectorTag(Encoding.UTF8.GetBytes(isrc.Value),
+ isrc.Key,
+ false,
+ SectorTagType.CdTrackIsrc);
+ }
+
+ // Write track flags
+ if(trackFlags.Count > 0)
+ {
+ foreach((byte track, byte flags) in trackFlags)
+ outputOptical.WriteSectorTag([flags], track, false, SectorTagType.CdTrackFlags);
+ }
+
+ // Write MCN
+ if(mcn != null) outputOptical.WriteMediaTag(Encoding.UTF8.GetBytes(mcn), MediaTagType.CD_MCN);
+
+ UpdateStatus?.Invoke(string.Format(UI.Will_copy_0_sectors_from_secondary_image,
+ sectorsToCopyFromSecondImage.Count));
+
+ // Copy sectors from secondary image
+ errno = CopyOpticalSectorsSecondary(secondaryOptical, outputOptical, useLong, sectorsToCopyFromSecondImage);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ errno = CopyOpticalSectorsTagsSecondary(secondaryOptical,
+ outputOptical,
+ useLong,
+ isrcs,
+ ref mcn,
+ tracks,
+ subchannelExtents,
+ smallestPregapLbaPerTrack,
+ sectorsToCopyFromSecondImage);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+
+ if(!IsCompactDiscMedia(primaryOptical.Info.MediaType) || !generateSubchannels) return ErrorNumber.NoError;
+
+ // Generate subchannel data
+ CompactDisc.GenerateSubchannels(subchannelExtents,
+ tracks,
+ trackFlags,
+ primaryOptical.Info.Sectors,
+ null,
+ InitProgress,
+ UpdateProgress,
+ EndProgress,
+ outputOptical);
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopyOpticalSectorsPrimary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
+ bool useLong)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+ InitProgress2?.Invoke();
+ byte[] generatedTitleKeys = null;
+ var currentTrack = 0;
+
+ foreach(Track track in inputOptical.Tracks)
+ {
+ if(_aborted) break;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_sectors_in_track_0_of_1,
+ currentTrack + 1,
+ inputOptical.Tracks.Count),
+ currentTrack,
+ inputOptical.Tracks.Count);
+
+ ulong doneSectors = 0;
+ ulong trackSectors = track.EndSector - track.StartSector + 1;
+
+ while(doneSectors < trackSectors)
+ {
+ if(_aborted) break;
+
+ byte[] sector;
+
+ uint sectorsToDo;
+
+ if(trackSectors - doneSectors >= (ulong)count)
+ sectorsToDo = (uint)count;
+ else
+ sectorsToDo = (uint)(trackSectors - doneSectors);
+
+ UpdateProgress2?.Invoke(string.Format(UI.Copying_sectors_0_to_1_in_track_2,
+ doneSectors + track.StartSector,
+ doneSectors + sectorsToDo + track.StartSector,
+ track.Sequence),
+ (long)doneSectors,
+ (long)trackSectors);
+
+ bool result;
+ SectorStatus sectorStatus = SectorStatus.NotDumped;
+ var sectorStatusArray = new SectorStatus[1];
+ ErrorNumber errno;
+
+ if(useLong)
+ {
+ errno = sectorsToDo == 1
+ ? inputOptical.ReadSectorLong(doneSectors + track.StartSector,
+ false,
+ out sector,
+ out sectorStatus)
+ : inputOptical.ReadSectorsLong(doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ out sector,
+ out sectorStatusArray);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ result = sectorsToDo == 1
+ ? outputOptical.WriteSectorLong(sector,
+ doneSectors + track.StartSector,
+ false,
+ sectorStatus)
+ : outputOptical.WriteSectorsLong(sector,
+ doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ sectorStatusArray);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors + track.StartSector));
+
+ return ErrorNumber.WriteError;
+ }
+
+ if(!result && sector.Length % 2352 != 0)
+ {
+ StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing);
+
+ return ErrorNumber.InOutError;
+ }
+ }
+ else
+ {
+ errno = sectorsToDo == 1
+ ? inputOptical.ReadSector(doneSectors + track.StartSector,
+ false,
+ out sector,
+ out sectorStatus)
+ : inputOptical.ReadSectors(doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ out sector,
+ out sectorStatusArray);
+
+ // TODO: Move to generic place when anything but CSS DVDs can be decrypted
+ if(IsDvdMedia(inputOptical.Info.MediaType) && decrypt)
+ {
+ DecryptDvdSector(ref sector,
+ inputOptical,
+ doneSectors + track.StartSector,
+ sectorsToDo,
+ _plugins,
+ ref generatedTitleKeys);
+ }
+
+ if(errno == ErrorNumber.NoError)
+ {
+ result = sectorsToDo == 1
+ ? outputOptical.WriteSector(sector,
+ doneSectors + track.StartSector,
+ false,
+ sectorStatus)
+ : outputOptical.WriteSectors(sector,
+ doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ sectorStatusArray);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors + track.StartSector));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ doneSectors + track.StartSector));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors += sectorsToDo;
+ }
+
+ currentTrack++;
+ }
+
+ EndProgress2?.Invoke();
+ EndProgress?.Invoke();
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopyOpticalSectorsSecondary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
+ bool useLong, List sectorsToCopy)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+ byte[] generatedTitleKeys = null;
+ int howManySectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors);
+ var howManySectorsCopied = 0;
+
+ foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors)
+ .TakeWhile(_ => !_aborted))
+ {
+ UpdateProgress?.Invoke(string.Format(UI.Copying_sector_0, sectorAddress),
+ howManySectorsCopied,
+ howManySectorsToCopy);
+
+ if(_aborted) break;
+
+ byte[] sector;
+ bool result;
+ SectorStatus sectorStatus;
+ ErrorNumber errno;
+
+ if(useLong)
+ {
+ errno = inputOptical.ReadSectorLong(sectorAddress, false, out sector, out sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputOptical.WriteSectorLong(sector, sectorAddress, false, sectorStatus);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ if(!result && sector.Length % 2352 != 0)
+ {
+ StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing);
+
+ return ErrorNumber.InOutError;
+ }
+ }
+
+ else
+ {
+ errno = inputOptical.ReadSector(sectorAddress, false, out sector, out sectorStatus);
+
+ // TODO: Move to generic place when anything but CSS DVDs can be decrypted
+ if(IsDvdMedia(inputOptical.Info.MediaType) && decrypt)
+ DecryptDvdSector(ref sector, inputOptical, sectorAddress, 1, _plugins, ref generatedTitleKeys);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputOptical.WriteSector(sector, sectorAddress, false, sectorStatus);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ howManySectorsCopied++;
+ }
+
+ EndProgress?.Invoke();
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopyOpticalSectorsTagsPrimary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
+ bool useLong, Dictionary isrcs, ref string mcn,
+ Track[] tracks, HashSet subchannelExtents,
+ Dictionary smallestPregapLbaPerTrack)
+ {
+ foreach(SectorTagType tag in inputOptical.Info.ReadableSectorTags.Order()
+ .TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ case SectorTagType.DvdSectorCmi:
+ case SectorTagType.DvdSectorTitleKey:
+ case SectorTagType.DvdSectorEdc:
+ case SectorTagType.DvdSectorIed:
+ case SectorTagType.DvdSectorInformation:
+ case SectorTagType.DvdSectorNumber:
+ // This tags are inline in long sector
+ continue;
+ }
+
+ InitProgress?.Invoke();
+ InitProgress2?.Invoke();
+ var currentTrack = 0;
+
+ foreach(Track track in inputOptical.Tracks)
+ {
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tags_in_track_0_of_1,
+ currentTrack + 1,
+ inputOptical.Tracks.Count),
+ currentTrack,
+ inputOptical.Tracks.Count);
+
+ ulong doneSectors = 0;
+ ulong trackSectors = track.EndSector - track.StartSector + 1;
+ byte[] sector;
+ bool result;
+
+ ErrorNumber errno;
+
+ switch(tag)
+ {
+ case SectorTagType.CdTrackFlags:
+ case SectorTagType.CdTrackIsrc:
+ errno = inputOptical.ReadSectorTag(track.Sequence, false, tag, out sector);
+
+ switch(errno)
+ {
+ case ErrorNumber.NoData:
+
+ continue;
+ case ErrorNumber.NoError:
+ result = outputOptical.WriteSectorTag(sector, track.Sequence, false, tag);
+
+ break;
+ default:
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_not_continuing,
+ outputOptical.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_not_continuing,
+ outputOptical.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+
+ continue;
+ }
+
+ while(doneSectors < trackSectors)
+ {
+ if(_aborted) break;
+
+ uint sectorsToDo;
+
+ if(trackSectors - doneSectors >= (ulong)count)
+ sectorsToDo = (uint)count;
+ else
+ sectorsToDo = (uint)(trackSectors - doneSectors);
+
+ UpdateProgress2?.Invoke(string.Format(UI.Copying_tag_3_for_sectors_0_to_1_in_track_2,
+ doneSectors + track.StartSector,
+ doneSectors + sectorsToDo + track.StartSector,
+ track.Sequence,
+ tag),
+ (long)(doneSectors + track.StartSector),
+ (long)(doneSectors + sectorsToDo + track.StartSector));
+
+ if(sectorsToDo == 1)
+ {
+ errno = inputOptical.ReadSectorTag(doneSectors + track.StartSector, false, tag, out sector);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(tag == SectorTagType.CdSectorSubchannel)
+ {
+ bool indexesChanged = CompactDisc.WriteSubchannelToImage(MmcSubchannel.Raw,
+ MmcSubchannel.Raw,
+ sector,
+ doneSectors + track.StartSector,
+ 1,
+ null,
+ isrcs,
+ (byte)track.Sequence,
+ ref mcn,
+ tracks,
+ subchannelExtents,
+ fixSubchannelPosition,
+ outputOptical,
+ fixSubchannel,
+ fixSubchannelCrc,
+ null,
+ smallestPregapLbaPerTrack,
+ false,
+ out _);
+
+ if(indexesChanged) outputOptical.SetTracks(tracks.ToList());
+
+ result = true;
+ }
+ else
+ {
+ result = outputOptical.WriteSectorTag(sector,
+ doneSectors + track.StartSector,
+ false,
+ tag);
+ }
+ }
+ else
+ {
+ StoppingErrorMessage
+ ?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_not_continuing,
+ errno,
+ doneSectors + track.StartSector));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = inputOptical.ReadSectorsTag(doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ tag,
+ out sector);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(tag == SectorTagType.CdSectorSubchannel)
+ {
+ bool indexesChanged = CompactDisc.WriteSubchannelToImage(MmcSubchannel.Raw,
+ MmcSubchannel.Raw,
+ sector,
+ doneSectors + track.StartSector,
+ sectorsToDo,
+ null,
+ isrcs,
+ (byte)track.Sequence,
+ ref mcn,
+ tracks,
+ subchannelExtents,
+ fixSubchannelPosition,
+ outputOptical,
+ fixSubchannel,
+ fixSubchannelCrc,
+ null,
+ smallestPregapLbaPerTrack,
+ false,
+ out _);
+
+ if(indexesChanged) outputOptical.SetTracks(tracks.ToList());
+
+ result = true;
+ }
+ else
+ {
+ result = outputOptical.WriteSectorsTag(sector,
+ doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ tag);
+ }
+ }
+ else
+ {
+ StoppingErrorMessage
+ ?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_not_continuing,
+ errno,
+ doneSectors + track.StartSector));
+
+ return errno;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_for_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ doneSectors + track.StartSector));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors += sectorsToDo;
+ }
+
+ currentTrack++;
+ }
+
+ EndProgress?.Invoke();
+ EndProgress2?.Invoke();
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopyOpticalSectorsTagsSecondary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
+ bool useLong, Dictionary isrcs, ref string mcn,
+ Track[] tracks, HashSet subchannelExtents,
+ Dictionary smallestPregapLbaPerTrack,
+ List sectorsToCopy)
+ {
+ foreach(SectorTagType tag in inputOptical.Info.ReadableSectorTags.Order()
+ .TakeWhile(_ => useLong)
+ .TakeWhile(_ => !_aborted))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ case SectorTagType.DvdSectorCmi:
+ case SectorTagType.DvdSectorTitleKey:
+ case SectorTagType.DvdSectorEdc:
+ case SectorTagType.DvdSectorIed:
+ case SectorTagType.DvdSectorInformation:
+ case SectorTagType.DvdSectorNumber:
+ // This tags are inline in long sector
+ continue;
+ }
+
+ InitProgress?.Invoke();
+
+
+ int numberOfSectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors);
+ var currentSectorIndex = 0;
+
+ foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors))
+ {
+ if(_aborted) break;
+
+ Track track = inputOptical.Tracks.FirstOrDefault(t => t.StartSector <= sectorAddress &&
+ t.EndSector >= sectorAddress);
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_0_for_sector_1, tag, sectorAddress),
+ currentSectorIndex,
+ numberOfSectorsToCopy);
+
+ ErrorNumber errno = inputOptical.ReadSectorTag(sectorAddress, false, tag, out byte[] sector);
+
+ bool result;
+
+ if(errno == ErrorNumber.NoError)
+ {
+ if(tag == SectorTagType.CdSectorSubchannel)
+ {
+ bool indexesChanged = CompactDisc.WriteSubchannelToImage(MmcSubchannel.Raw,
+ MmcSubchannel.Raw,
+ sector,
+ sectorAddress,
+ 1,
+ null,
+ isrcs,
+ (byte)track.Sequence,
+ ref mcn,
+ tracks,
+ subchannelExtents,
+ fixSubchannelPosition,
+ outputOptical,
+ fixSubchannel,
+ fixSubchannelCrc,
+ null,
+ smallestPregapLbaPerTrack,
+ false,
+ out _);
+
+ if(indexesChanged) outputOptical.SetTracks(tracks.ToList());
+
+ result = true;
+ }
+ else
+ result = outputOptical.WriteSectorTag(sector, sectorAddress, false, tag);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_for_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ currentSectorIndex++;
+ }
+
+ EndProgress?.Invoke();
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ /// Decrypts DVD sectors using CSS (Content Scramble System) decryption
+ /// Retrieves decryption keys from sector tags or generates them from ISO9660 filesystem
+ /// Only MPEG packets within sectors can be encrypted
+ ///
+ void DecryptDvdSector(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress, uint sectorsToDo,
+ PluginRegister plugins, ref byte[] generatedTitleKeys)
+ {
+ if(_aborted) return;
+
+ // Only sectors which are MPEG packets can be encrypted.
+ if(!Mpeg.ContainsMpegPackets(sector, sectorsToDo)) return;
+
+ byte[] cmi, titleKey;
+
+ if(sectorsToDo == 1)
+ {
+ if(inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdSectorCmi, out cmi) ==
+ ErrorNumber.NoError &&
+ inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdTitleKeyDecrypted, out titleKey) ==
+ ErrorNumber.NoError)
+ sector = CSS.DecryptSector(sector, titleKey, cmi);
+ else
+ {
+ if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys);
+
+ if(generatedTitleKeys != null)
+ {
+ sector = CSS.DecryptSector(sector,
+ generatedTitleKeys.Skip((int)(5 * sectorAddress)).Take(5).ToArray(),
+ null);
+ }
+ }
+ }
+ else
+ {
+ if(inputOptical.ReadSectorsTag(sectorAddress, false, sectorsToDo, SectorTagType.DvdSectorCmi, out cmi) ==
+ ErrorNumber.NoError &&
+ inputOptical.ReadSectorsTag(sectorAddress,
+ false,
+ sectorsToDo,
+ SectorTagType.DvdTitleKeyDecrypted,
+ out titleKey) ==
+ ErrorNumber.NoError)
+ sector = CSS.DecryptSector(sector, titleKey, cmi, sectorsToDo);
+ else
+ {
+ if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys);
+
+ if(generatedTitleKeys != null)
+ {
+ sector = CSS.DecryptSector(sector,
+ generatedTitleKeys.Skip((int)(5 * sectorAddress))
+ .Take((int)(5 * sectorsToDo))
+ .ToArray(),
+ null,
+ sectorsToDo);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Generates DVD CSS title keys from ISO9660 filesystem
+ /// Used when explicit title keys are not available in sector tags
+ /// Searches for ISO9660 partitions to derive decryption keys
+ ///
+ void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, ref byte[] generatedTitleKeys)
+ {
+ if(_aborted) return;
+
+ List partitions = Partitions.GetAll(inputOptical);
+
+ partitions = partitions.FindAll(p =>
+ {
+ Filesystems.Identify(inputOptical, out List idPlugins, p);
+
+ return idPlugins.Contains("iso9660 filesystem");
+ });
+
+ if(!plugins.ReadOnlyFilesystems.TryGetValue("iso9660 filesystem", out IReadOnlyFilesystem rofs)) return;
+
+ AaruLogging.Debug(MODULE_NAME, UI.Generating_decryption_keys);
+
+ generatedTitleKeys = CSS.GenerateTitleKeys(inputOptical, partitions, inputOptical.Info.Sectors, rofs);
+ }
+
+ static bool IsDvdMedia(MediaType mediaType) =>
+
+ // Checks if media type is any variant of DVD (ROM, R, RDL, PR, PRDL)
+ // Consolidates media type checking logic used throughout conversion process
+ mediaType is MediaType.DVDROM or MediaType.DVDR or MediaType.DVDRDL or MediaType.DVDPR or MediaType.DVDPRDL;
+
+ private static bool IsCompactDiscMedia(MediaType mediaType) =>
+
+ // Checks if media type is any variant of compact disc (CD, CDDA, CDR, CDRW, etc.)
+ // Covers all 45+ CD-based media types including gaming and specialty formats
+ mediaType is MediaType.CD
+ or MediaType.CDDA
+ or MediaType.CDG
+ or MediaType.CDEG
+ or MediaType.CDI
+ or MediaType.CDROM
+ or MediaType.CDROMXA
+ or MediaType.CDPLUS
+ or MediaType.CDMO
+ or MediaType.CDR
+ or MediaType.CDRW
+ or MediaType.CDMRW
+ or MediaType.VCD
+ or MediaType.SVCD
+ or MediaType.PCD
+ or MediaType.DTSCD
+ or MediaType.CDMIDI
+ or MediaType.CDV
+ or MediaType.CDIREADY
+ or MediaType.FMTOWNS
+ or MediaType.PS1CD
+ or MediaType.PS2CD
+ or MediaType.MEGACD
+ or MediaType.SATURNCD
+ or MediaType.GDROM
+ or MediaType.GDR
+ or MediaType.MilCD
+ or MediaType.SuperCDROM2
+ or MediaType.JaguarCD
+ or MediaType.ThreeDO
+ or MediaType.PCFX
+ or MediaType.NeoGeoCD
+ or MediaType.CDTV
+ or MediaType.CD32
+ or MediaType.Playdia
+ or MediaType.Pippin
+ or MediaType.VideoNow
+ or MediaType.VideoNowColor
+ or MediaType.VideoNowXp
+ or MediaType.CVD;
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Parser.cs b/Aaru.Core/Image/Merge/Parser.cs
new file mode 100644
index 000000000..4c96e37ac
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Parser.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ static (List overrideSectors, List overrideNegativeSectors) ParseOverrideSectorsList(
+ string overrideSectorsListPath)
+ {
+ if(overrideSectorsListPath is null) return ([], []);
+
+ List overrideSectors = [];
+ List overrideNegativeSectors = [];
+
+ StreamReader sr = new(overrideSectorsListPath);
+
+ while(sr.ReadLine() is {} line)
+ {
+ line = line.Trim();
+
+ if(line.Length == 0) continue;
+
+ if(line.StartsWith('-'))
+ {
+ if(long.TryParse(line[1..], CultureInfo.InvariantCulture, out long negativeSector))
+ overrideNegativeSectors.Add((uint)(negativeSector * -1));
+ }
+ else
+ {
+ if(ulong.TryParse(line, CultureInfo.InvariantCulture, out ulong sector)) overrideSectors.Add(sector);
+ }
+ }
+
+ sr.Close();
+
+ return (overrideSectors, overrideNegativeSectors);
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Sectors.cs b/Aaru.Core/Image/Merge/Sectors.cs
new file mode 100644
index 000000000..c200b8456
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Sectors.cs
@@ -0,0 +1,325 @@
+using System.Collections.Generic;
+using System.Linq;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber CopySectorsPrimary(bool useLong, bool isTape, IMediaImage primaryImage, IWritableImage outputImage)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+ ulong doneSectors = 0;
+
+ while(doneSectors < primaryImage.Info.Sectors)
+ {
+ byte[] sector;
+
+ uint sectorsToDo;
+
+ if(isTape)
+ sectorsToDo = 1;
+ else if(primaryImage.Info.Sectors - doneSectors >= (ulong)count)
+ sectorsToDo = (uint)count;
+ else
+ sectorsToDo = (uint)(primaryImage.Info.Sectors - doneSectors);
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_sectors_0_to_1, doneSectors, doneSectors + sectorsToDo),
+ (long)doneSectors,
+ (long)primaryImage.Info.Sectors);
+
+ bool result;
+ SectorStatus sectorStatus = SectorStatus.NotDumped;
+ var sectorStatusArray = new SectorStatus[1];
+
+ ErrorNumber errno;
+
+ if(useLong)
+ {
+ errno = sectorsToDo == 1
+ ? primaryImage.ReadSectorLong(doneSectors, false, out sector, out sectorStatus)
+ : primaryImage.ReadSectorsLong(doneSectors,
+ false,
+ sectorsToDo,
+ out sector,
+ out sectorStatusArray);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ result = sectorsToDo == 1
+ ? outputImage.WriteSectorLong(sector, doneSectors, false, sectorStatus)
+ : outputImage.WriteSectorsLong(sector,
+ doneSectors,
+ false,
+ sectorsToDo,
+ sectorStatusArray);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = sectorsToDo == 1
+ ? primaryImage.ReadSector(doneSectors, false, out sector, out sectorStatus)
+ : primaryImage.ReadSectors(doneSectors,
+ false,
+ sectorsToDo,
+ out sector,
+ out sectorStatusArray);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ result = sectorsToDo == 1
+ ? outputImage.WriteSector(sector, doneSectors, false, sectorStatus)
+ : outputImage.WriteSectors(sector, doneSectors, false, sectorsToDo, sectorStatusArray);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors));
+
+ return errno;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ doneSectors));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors += sectorsToDo;
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopySectorsTagPrimary(bool useLong, IMediaImage primaryImage, IWritableImage outputImage)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ foreach(SectorTagType tag in primaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ }
+
+
+ ulong doneSectors = 0;
+
+ InitProgress?.Invoke();
+
+ while(doneSectors < primaryImage.Info.Sectors)
+ {
+ uint sectorsToDo;
+
+ if(primaryImage.Info.Sectors - doneSectors >= (ulong)count)
+ sectorsToDo = (uint)count;
+ else
+ sectorsToDo = (uint)(primaryImage.Info.Sectors - doneSectors);
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_2_for_sectors_0_to_1,
+ doneSectors,
+ doneSectors + sectorsToDo,
+ tag),
+ (long)doneSectors,
+ (long)primaryImage.Info.Sectors);
+
+ bool result;
+
+ ErrorNumber errno = sectorsToDo == 1
+ ? primaryImage.ReadSectorTag(doneSectors, false, tag, out byte[] sector)
+ : primaryImage.ReadSectorsTag(doneSectors, false, sectorsToDo, tag, out sector);
+
+ if(errno == ErrorNumber.NoError)
+ {
+ result = sectorsToDo == 1
+ ? outputImage.WriteSectorTag(sector, doneSectors, false, tag)
+ : outputImage.WriteSectorsTag(sector, doneSectors, false, sectorsToDo, tag);
+ }
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors));
+
+ return errno;
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ doneSectors));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors += sectorsToDo;
+ }
+
+ EndProgress?.Invoke();
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopySectorsSecondary(bool useLong, bool isTape, IMediaImage secondaryImage, IWritableImage outputImage,
+ List sectorsToCopy)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+ var doneSectors = 0;
+ int totalSectorsToCopy = sectorsToCopy.Count(t => t < secondaryImage.Info.Sectors);
+
+ foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < secondaryImage.Info.Sectors))
+ {
+ byte[] sector;
+
+ UpdateProgress?.Invoke(string.Format(UI.Copying_sector_0, sectorAddress), doneSectors, totalSectorsToCopy);
+
+ bool result;
+
+ ErrorNumber errno;
+
+ if(useLong)
+ {
+ errno = secondaryImage.ReadSectorLong(sectorAddress, false, out sector, out SectorStatus sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorLong(sector, sectorAddress, false, sectorStatus);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors));
+
+ return errno;
+ }
+ }
+ else
+ {
+ errno = secondaryImage.ReadSector(sectorAddress, false, out sector, out SectorStatus sectorStatus);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSector(sector, sectorAddress, false, sectorStatus);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors));
+
+ return errno;
+ }
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ doneSectors));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors++;
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ ErrorNumber CopySectorsTagSecondary(bool useLong, IMediaImage secondaryImage, IWritableImage outputImage,
+ List sectorsToCopy)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ foreach(SectorTagType tag in secondaryImage.Info.ReadableSectorTags.TakeWhile(_ => useLong))
+ {
+ switch(tag)
+ {
+ case SectorTagType.AppleSonyTag:
+ case SectorTagType.AppleProfileTag:
+ case SectorTagType.PriamDataTowerTag:
+ case SectorTagType.CdSectorSync:
+ case SectorTagType.CdSectorHeader:
+ case SectorTagType.CdSectorSubHeader:
+ case SectorTagType.CdSectorEdc:
+ case SectorTagType.CdSectorEccP:
+ case SectorTagType.CdSectorEccQ:
+ case SectorTagType.CdSectorEcc:
+ // These tags are inline in long sector
+ continue;
+ }
+
+
+ var doneSectors = 0;
+ int sectorsToCopyCount = sectorsToCopy.Count(t => t < secondaryImage.Info.Sectors);
+
+ InitProgress?.Invoke();
+
+ foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < secondaryImage.Info.Sectors))
+ {
+ UpdateProgress?.Invoke(string.Format(UI.Copying_tag_0_for_sector_1, tag, sectorAddress),
+ doneSectors,
+ sectorsToCopyCount);
+
+ bool result;
+
+ ErrorNumber errno = secondaryImage.ReadSectorTag(sectorAddress, false, tag, out byte[] sector);
+
+ if(errno == ErrorNumber.NoError)
+ result = outputImage.WriteSectorTag(sector, sectorAddress, false, tag);
+ else
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ sectorAddress));
+
+ return errno;
+ }
+
+ if(!result)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputImage.ErrorMessage,
+ sectorAddress));
+
+ return ErrorNumber.WriteError;
+ }
+
+ doneSectors++;
+ }
+
+ EndProgress?.Invoke();
+ }
+
+ return ErrorNumber.NoError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Tags.cs b/Aaru.Core/Image/Merge/Tags.cs
new file mode 100644
index 000000000..93f373688
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Tags.cs
@@ -0,0 +1,63 @@
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+using Humanizer;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber CopyMediaTags(IMediaImage primaryImage, IMediaImage secondaryImage, IWritableImage outputFormat)
+ {
+ if(_aborted) return ErrorNumber.NoError;
+
+ InitProgress?.Invoke();
+
+ foreach(MediaTagType mediaTag in primaryImage.Info.ReadableMediaTags)
+ {
+ PulseProgress?.Invoke(string.Format(UI.Copying_media_tag_0_from_primary_image, mediaTag.Humanize()));
+
+ ErrorNumber errno = primaryImage.ReadMediaTag(mediaTag, out byte[] tag);
+
+ if(errno != ErrorNumber.NoError)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_media_tag_not_continuing, errno));
+
+ return errno;
+ }
+
+ if(outputFormat?.WriteMediaTag(tag, mediaTag) == true) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_media_tag_not_continuing,
+ outputFormat?.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+
+ foreach(MediaTagType mediaTag in secondaryImage.Info.ReadableMediaTags)
+ {
+ if(!useSecondaryTags && primaryImage.Info.ReadableMediaTags.Contains(mediaTag)) continue;
+
+ PulseProgress?.Invoke(string.Format(UI.Copying_media_tag_0_from_secondary_image, mediaTag.Humanize()));
+
+ ErrorNumber errno = secondaryImage.ReadMediaTag(mediaTag, out byte[] tag);
+
+ if(errno != ErrorNumber.NoError)
+ {
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_media_tag_not_continuing, errno));
+
+ return errno;
+ }
+
+ if(outputFormat?.WriteMediaTag(tag, mediaTag) == true) continue;
+
+ StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_media_tag_not_continuing,
+ outputFormat?.ErrorMessage));
+
+ return ErrorNumber.WriteError;
+ }
+
+ EndProgress?.Invoke();
+ return ErrorNumber.NoError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Core/Image/Merge/Tape.cs b/Aaru.Core/Image/Merge/Tape.cs
new file mode 100644
index 000000000..b96455be6
--- /dev/null
+++ b/Aaru.Core/Image/Merge/Tape.cs
@@ -0,0 +1,36 @@
+using System;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public sealed partial class Merger
+{
+ ErrorNumber ValidateTapeImage(ITapeImage primaryTape, ITapeImage secondaryTape, IWritableTapeImage outputTape)
+ {
+ if(_aborted || primaryTape?.IsTape != true && secondaryTape?.IsTape != true || outputTape is not null)
+ return ErrorNumber.NoError;
+
+ StoppingErrorMessage?.Invoke(UI.Input_format_contains_a_tape_image_and_is_not_supported_by_output_format);
+
+ return ErrorNumber.UnsupportedMedia;
+ }
+
+ ErrorNumber SetupTapeImage(ITapeImage primaryTape, ITapeImage secondaryTape, IWritableTapeImage outputTape)
+ {
+ if(_aborted || primaryTape?.IsTape != true && secondaryTape?.IsTape != true || outputTape == null)
+ return ErrorNumber.NoError;
+
+ bool ret = outputTape.SetTape();
+
+ // Cannot set image to tape mode
+ if(ret) return ErrorNumber.NoError;
+
+ StoppingErrorMessage?.Invoke(UI.Error_setting_output_image_in_tape_mode +
+ Environment.NewLine +
+ outputTape.ErrorMessage);
+
+ return ErrorNumber.WriteError;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Localization/Core.Designer.cs b/Aaru.Localization/Core.Designer.cs
index 0cdd4ad2a..77e5e3b49 100644
--- a/Aaru.Localization/Core.Designer.cs
+++ b/Aaru.Localization/Core.Designer.cs
@@ -6898,5 +6898,11 @@ namespace Aaru.Localization {
return ResourceManager.GetString("DVD_2nd_layer_Physical_Format_Information_contained_in_image_WithMarkup", resourceCulture);
}
}
+
+ public static string Media_tag_0_present_in_primary_image_will_be_lost {
+ get {
+ return ResourceManager.GetString("Media_tag_0_present_in_primary_image_will_be_lost", resourceCulture);
+ }
+ }
}
}
diff --git a/Aaru.Localization/Core.es.resx b/Aaru.Localization/Core.es.resx
index b12b1be17..3f9dc3f66 100644
--- a/Aaru.Localization/Core.es.resx
+++ b/Aaru.Localization/Core.es.resx
@@ -3499,4 +3499,7 @@ No tiene sentido hacerlo y supondría demasiado esfuerzo para la cinta.
[bold][blue]Información del formato físico del DVD (2ª capa) contenida en la imagen:[/][/]
+
+ [red]La etiqueta del medio {0} presente en la imagen primaria se perderá en el formato de salida. No se continuará...[/]
+
\ No newline at end of file
diff --git a/Aaru.Localization/Core.resx b/Aaru.Localization/Core.resx
index e8a9ac553..c5a5539c2 100644
--- a/Aaru.Localization/Core.resx
+++ b/Aaru.Localization/Core.resx
@@ -3520,4 +3520,7 @@ It has no sense to do it, and it will put too much strain on the tape.
[bold][blue]DVD 2nd layer Physical Format Information contained in image:[/][/]
+
+ [red]Media tag {0} present in primary image will be lost in output format. Not continuing...[/]
+
\ No newline at end of file
diff --git a/Aaru.Localization/UI.Designer.cs b/Aaru.Localization/UI.Designer.cs
index 8a5b853e9..6a3b2d865 100644
--- a/Aaru.Localization/UI.Designer.cs
+++ b/Aaru.Localization/UI.Designer.cs
@@ -10509,5 +10509,271 @@ namespace Aaru.Localization {
return ResourceManager.GetString("Use_long_sectors", resourceCulture);
}
}
+
+ public static string Image_Merge_Command_Description {
+ get {
+ return ResourceManager.GetString("Image_Merge_Command_Description", resourceCulture);
+ }
+ }
+
+ public static string Path_to_the_primary_image_file {
+ get {
+ return ResourceManager.GetString("Path_to_the_primary_image_file", resourceCulture);
+ }
+ }
+
+ public static string Path_to_the_secondary_image_file {
+ get {
+ return ResourceManager.GetString("Path_to_the_secondary_image_file", resourceCulture);
+ }
+ }
+
+ public static string Path_to_the_output_merged_image_file {
+ get {
+ return ResourceManager.GetString("Path_to_the_output_merged_image_file", resourceCulture);
+ }
+ }
+
+ public static string Use_media_tags_from_secondary_image {
+ get {
+ return ResourceManager.GetString("Use_media_tags_from_secondary_image", resourceCulture);
+ }
+ }
+
+ public static string File_containing_list_of_sectors_to_take_from_secondary_image {
+ get {
+ return ResourceManager.GetString("File_containing_list_of_sectors_to_take_from_secondary_image", resourceCulture);
+ }
+ }
+
+ public static string Ignore_mismatched_image_media_type {
+ get {
+ return ResourceManager.GetString("Ignore_mismatched_image_media_type", resourceCulture);
+ }
+ }
+
+ public static string Resume_file_for_primary_image {
+ get {
+ return ResourceManager.GetString("Resume_file_for_primary_image", resourceCulture);
+ }
+ }
+
+ public static string Resume_file_for_secondary_image {
+ get {
+ return ResourceManager.GetString("Resume_file_for_secondary_image", resourceCulture);
+ }
+ }
+
+ public static string Media_tag_0_present_in_primary_image_will_be_lost_in_output_format {
+ get {
+ return ResourceManager.GetString("Media_tag_0_present_in_primary_image_will_be_lost_in_output_format", resourceCulture);
+ }
+ }
+
+ public static string Media_tag_0_present_in_secondary_image_will_be_lost_in_output_format {
+ get {
+ return ResourceManager.GetString("Media_tag_0_present_in_secondary_image_will_be_lost_in_output_format", resourceCulture);
+ }
+ }
+
+ public static string Output_image_does_not_support_sector_tag_0_data_will_be_lost {
+ get {
+ return ResourceManager.GetString("Output_image_does_not_support_sector_tag_0_data_will_be_lost", resourceCulture);
+ }
+ }
+
+ public static string Copying_negative_sector_0_of_1 {
+ get {
+ return ResourceManager.GetString("Copying_negative_sector_0_of_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_tag_1_for_negative_sector_0 {
+ get {
+ return ResourceManager.GetString("Copying_tag_1_for_negative_sector_0", resourceCulture);
+ }
+ }
+
+ public static string Copying_negative_sector_0 {
+ get {
+ return ResourceManager.GetString("Copying_negative_sector_0", resourceCulture);
+ }
+ }
+
+ public static string Copying_overflow_sector_0_of_1 {
+ get {
+ return ResourceManager.GetString("Copying_overflow_sector_0_of_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_tag_1_for_overflow_sector_0 {
+ get {
+ return ResourceManager.GetString("Copying_tag_1_for_overflow_sector_0", resourceCulture);
+ }
+ }
+
+ public static string Copying_overflow_sector_0 {
+ get {
+ return ResourceManager.GetString("Copying_overflow_sector_0", resourceCulture);
+ }
+ }
+
+ public static string Images_have_different_media_types_cannot_merge {
+ get {
+ return ResourceManager.GetString("Images_have_different_media_types_cannot_merge", resourceCulture);
+ }
+ }
+
+ public static string Images_have_different_number_of_sectors_cannot_merge {
+ get {
+ return ResourceManager.GetString("Images_have_different_number_of_sectors_cannot_merge", resourceCulture);
+ }
+ }
+
+ public static string Parsing_sectors_file {
+ get {
+ return ResourceManager.GetString("Parsing_sectors_file", resourceCulture);
+ }
+ }
+
+ public static string Sectors_file_contains_invalid_sector_number_0_not_continuing {
+ get {
+ return ResourceManager.GetString("Sectors_file_contains_invalid_sector_number_0_not_continuing", resourceCulture);
+ }
+ }
+
+ public static string Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_continuing {
+ get {
+ return ResourceManager.GetString("Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_con" +
+ "tinuing", resourceCulture);
+ }
+ }
+
+ public static string Calculating_sectors_to_merge {
+ get {
+ return ResourceManager.GetString("Calculating_sectors_to_merge", resourceCulture);
+ }
+ }
+
+ public static string No_sectors_to_merge__output_image_will_be_identical_to_primary_image_not_continuing {
+ get {
+ return ResourceManager.GetString("No_sectors_to_merge__output_image_will_be_identical_to_primary_image_not_continui" +
+ "ng", resourceCulture);
+ }
+ }
+
+ public static string Calculating_merged_dump_hardware_list {
+ get {
+ return ResourceManager.GetString("Calculating_merged_dump_hardware_list", resourceCulture);
+ }
+ }
+
+ public static string Copying_0_sectors_from_primary_image {
+ get {
+ return ResourceManager.GetString("Copying_0_sectors_from_primary_image", resourceCulture);
+ }
+ }
+
+ public static string Will_copy_0_sectors_from_secondary_image {
+ get {
+ return ResourceManager.GetString("Will_copy_0_sectors_from_secondary_image", resourceCulture);
+ }
+ }
+
+ public static string Flux_data_will_be_copied_as_is_from_primary_image {
+ get {
+ return ResourceManager.GetString("Flux_data_will_be_copied_as_is_from_primary_image", resourceCulture);
+ }
+ }
+
+ public static string Copying_file_0_of_partition_1 {
+ get {
+ return ResourceManager.GetString("Copying_file_0_of_partition_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_tape_partition_0 {
+ get {
+ return ResourceManager.GetString("Copying_tape_partition_0", resourceCulture);
+ }
+ }
+
+ public static string Merge_completed_successfully {
+ get {
+ return ResourceManager.GetString("Merge_completed_successfully", resourceCulture);
+ }
+ }
+
+ public static string Decrypting_encrypted_sectors {
+ get {
+ return ResourceManager.GetString("Decrypting_encrypted_sectors", resourceCulture);
+ }
+ }
+
+ public static string Copying_sectors_in_track_0_of_1 {
+ get {
+ return ResourceManager.GetString("Copying_sectors_in_track_0_of_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_sectors_0_to_1_in_track_2 {
+ get {
+ return ResourceManager.GetString("Copying_sectors_0_to_1_in_track_2", resourceCulture);
+ }
+ }
+
+ public static string Input_image_is_not_returning_long_sectors_not_continuing {
+ get {
+ return ResourceManager.GetString("Input_image_is_not_returning_long_sectors_not_continuing", resourceCulture);
+ }
+ }
+
+ public static string Copying_sector_0 {
+ get {
+ return ResourceManager.GetString("Copying_sector_0", resourceCulture);
+ }
+ }
+
+ public static string Copying_tags_in_track_0_of_1 {
+ get {
+ return ResourceManager.GetString("Copying_tags_in_track_0_of_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_tag_3_for_sectors_0_to_1_in_track_2 {
+ get {
+ return ResourceManager.GetString("Copying_tag_3_for_sectors_0_to_1_in_track_2", resourceCulture);
+ }
+ }
+
+ public static string Copying_tag_0_for_sector_1 {
+ get {
+ return ResourceManager.GetString("Copying_tag_0_for_sector_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_sectors_0_to_1 {
+ get {
+ return ResourceManager.GetString("Copying_sectors_0_to_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_tag_2_for_sectors_0_to_1 {
+ get {
+ return ResourceManager.GetString("Copying_tag_2_for_sectors_0_to_1", resourceCulture);
+ }
+ }
+
+ public static string Copying_media_tag_0_from_primary_image {
+ get {
+ return ResourceManager.GetString("Copying_media_tag_0_from_primary_image", resourceCulture);
+ }
+ }
+
+ public static string Copying_media_tag_0_from_secondary_image {
+ get {
+ return ResourceManager.GetString("Copying_media_tag_0_from_secondary_image", resourceCulture);
+ }
+ }
}
}
diff --git a/Aaru.Localization/UI.es.resx b/Aaru.Localization/UI.es.resx
index 43a7d6e1f..5497b1f49 100644
--- a/Aaru.Localization/UI.es.resx
+++ b/Aaru.Localization/UI.es.resx
@@ -5255,4 +5255,138 @@ Probadores:
Usar sectores largos (con etiquetas).
+
+ Combina dos imágenes en una
+
+
+ Ruta al archivo de imagen primario.
+
+
+ Ruta al archivo de imagen secundario.
+
+
+ Ruta al archivo de imagen combinado.
+
+
+ Usa las etiquetas del medio de la imagen secundaria (de otra forma cuando estén en ambas imágenes, se usarán las de la primaria).
+
+
+ Archivo que contiene la lista de sectores a obtener de la imagen secundaria (un sector por línea).
+
+
+ Ignorar los tipos de medio no coincidentes. La imagen combinada tendrá el tipo de medio de la imagen primaria.
+
+
+ Fichero de resumen para la imagen primaria
+
+
+ Fichero de resumen para la imagen secundaria
+
+
+ [red]La etiqueta del medio [orange3]{0}[/] presente en la imagen primaria se perderá en el formato de salida. No se continuará...[/]
+
+
+ [red]La etiqueta del medio [orange3]{0}[/] presente en la imagen secundaria se perderá en el formato de salida. No se continuará...[/]
+
+
+ [red]La imagen de salida no soporta la etiqueta de sector [orange3]{0}[/], se perderán datos. No se continuará...[/]
+
+
+ [slateblue1]Copiando sector [lime]-{0}[/] de [violet]-{1}[/][/]
+
+
+ [slateblue1]Copiando etiqueta [orange3]{1}[/] para el sector [lime]-{0}[/][/]
+
+
+ [slateblue1]Copiando sector [lime]-{0}[/][/]
+
+
+ [slateblue1]Copiando sector de sobrecarga [lime]{0}[/] de [violet]{1}[/][/]
+
+
+ [slateblue1]Copiando etiqueta [orange3]{1}[/] para el sector de sobrecarga [lime]{0}[/][/]
+
+
+ [slateblue1]Copiando sector de sobrecarga [lime]{0}[/][/]
+
+
+ [red]Las imágenes tienen diferentes tipos de medio, no se pueden combinar.[/]
+
+
+ [red]Las imágenes tienen diferente número de sectores, no se pueden combinar.[/]
+
+
+ [slateblue1]Interpretando el fichero de sectores...[/]
+
+
+ [red]El fichero de sectores contiene el sector inválido 0, no se continuará...[/]
+
+
+ [red]El fichero de sectores contiene el sector [lime]{0}[/] que excede el sector máximo permitido [violet]{1}[/], no se continuará...[/]
+
+
+ [slateblue1]Calculando sectores a combinar...[/]
+
+
+ [red]No hay sectores a combinar, la imagen de salida sería idéntica a la primaria, no se continuará...[/]
+
+
+ [slateblue1]Calculando list de hardware de volcado combinada...[/]
+
+
+ [slateblue1]Copiando [teal]{0}[/] sectores de la imagen primaria[/]
+
+
+ [slateblue1]Se copiarán [teal]{0}[/] sectores de la imagen secundaria.[/]
+
+
+ [slateblue1]Los datos de flujo se copiarán tal cual de la imagen primaria...[/]
+
+
+ [slateblue1]Copiando fichero [lime]{0}[/] de partición [teal]{1}[/]...[/]
+
+
+ [slateblue1]Copiando partición de cinta [teal]{0}[/]...[/]
+
+
+ [green]¡Combinación completada con éxito![/]
+
+
+ [green]Desencriptando sectores encriptados.[/]
+
+
+ [slateblue1]Copiando sectores en pista [teal]{0}[/] de [teal]{1}[/][/]
+
+
+ [slateblue1]Copiando sectores del [lime]{0}[/] al [violet]{1}[/] en la pista [teal]{2}[/][/]
+
+
+ [red]El formato de imagen de entrada no está devolviendo sectores sin procesar, no se continuará...[/]
+
+
+ [slateblue1]Copiando sector [lime]{0}[/][/]
+
+
+ [slateblue1]Copiando etiquetas en la pista [teal]{0}[/] de [teal]{1}[/][/]
+
+
+ [slateblue1]Copiando etiqueta [orange3]{3}[/] para los sectores del [lime]{0}[/] al [violet]{1}[/] en la pista [teal]{2}[/][/]
+
+
+ [slateblue1]Copiando etiqueta [orange3]{0}[/] para el sector [lime]{1}[/][/]
+
+
+ [slateblue1]Copiando sectores del [lime]{0}[/] al [violet]{1}[/][/]
+
+
+ [slateblue1]Copiando etiqueta [orange3]{2}[/] para los sectores del [lime]{0}[/] al [violet]{1}[/][/]
+
+
+ [slateblue1]Copiando etiqueta del medio [orange3]{0}[/] de la imagen primaria[/]
+
+
+ [slateblue1]Copiando etiqueta del medio [orange3]{0}[/] de la imagen secundaria[/]
+
\ No newline at end of file
diff --git a/Aaru.Localization/UI.resx b/Aaru.Localization/UI.resx
index c4339f026..aa0e412fe 100644
--- a/Aaru.Localization/UI.resx
+++ b/Aaru.Localization/UI.resx
@@ -5333,4 +5333,138 @@ Do you want to continue?
Use long sectors (with tags).
+
+ Merges two images into one
+
+
+ Path to the primary image file.
+
+
+ Path to the secondary image file.
+
+
+ Path to the output merged image file.
+
+
+ Use media tags from secondary image (otherwise when in both images, primary image tags are used).
+
+
+ File containing list of sectors to take from secondary image (one sector number per line).
+
+
+ Ignore mismatched image media type. Merged image will still have primary image media type.
+
+
+ Resume file for primary image
+
+
+ Resume file for secondary image
+
+
+ [red]Media tag [orange3]{0}[/] present in primary image will be lost in output format. Not continuing...[/]
+
+
+ [red]Media tag [orange3]{0}[/] present in secondary image will be lost in output format. Not continuing...[/]
+
+
+ [red]Output image does not support sector tag [orange3]{0}[/], data will be lost. Not continuing...[/]
+
+
+ [slateblue1]Copying sector [lime]-{0}[/] of [violet]-{1}[/][/]
+
+
+ [slateblue1]Copying tag [orange3]{1}[/] for sector [lime]-{0}[/][/]
+
+
+ [slateblue1]Copying sector [lime]-{0}[/][/]
+
+
+ [slateblue1]Copying overflow sector [lime]{0}[/] of [violet]{1}[/][/]
+
+
+ [slateblue1]Copying tag [orange3]{1}[/] for overflow sector [lime]{0}[/][/]
+
+
+ [slateblue1]Copying overflow sector [lime]{0}[/][/]
+
+
+ [red]Images have different media types, cannot merge.[/]
+
+
+ [red]Images have different number of sectors, cannot merge.[/]
+
+
+ [slateblue1]Parsing sectors file...[/]
+
+
+ [red]Sectors file contains invalid sector number 0, not continuing...[/]
+
+
+ [red]Sectors file contains sector [lime]{0}[/] which exceeds the maximum allowed sector [violet]{1}[/], not continuing...[/]
+
+
+ [slateblue1]Calculating sectors to merge...[/]
+
+
+ [red]No sectors to merge, output image will be identical to primary image, not continuing...[/]
+
+
+ [slateblue1]Calculating merged dump hardware list...[/]
+
+
+ [slateblue1]Copying [teal]{0}[/] sectors from primary image[/]
+
+
+ [slateblue1]Will copy [teal]{0}[/] sectors from secondary image.[/]
+
+
+ [slateblue1]Flux data will be copied as-is from primary image...[/]
+
+
+ [slateblue1]Copying file [lime]{0}[/] of partition [teal]{1}[/]...[/]
+
+
+ [slateblue1]Copying tape partition [teal]{0}[/]...[/]
+
+
+ [green]Merge completed successfully![/]
+
+
+ [green]Decrypting encrypted sectors.[/]
+
+
+ [slateblue1]Copying sectors in track [teal]{0}[/] of [teal]{1}[/][/]
+
+
+ [slateblue1]Copying sectors [lime]{0}[/] to [violet]{1}[/] in track [teal]{2}[/][/]
+
+
+ [red]Input image is not returning raw sectors, not continuing...[/]
+
+
+ [slateblue1]Copying sector [lime]{0}[/][/]
+
+
+ [slateblue1]Copying tags in track [teal]{0}[/] of [teal]{1}[/][/]
+
+
+ [slateblue1]Copying tag [orange3]{3}[/] for sectors [lime]{0}[/] to [violet]{1}[/] in track [teal]{2}[/][/]
+
+
+ [slateblue1]Copying tag [orange3]{0}[/] for sector [lime]{1}[/][/]
+
+
+ [slateblue1]Copying sectors [lime]{0}[/] to [violet]{1}[/][/]
+
+
+ [slateblue1]Copying tag [orange3]{2}[/] for sectors [lime]{0}[/] to [violet]{1}[/][/]
+
+
+ [slateblue1]Copying media tag [orange3]{0}[/] from primary image[/]
+
+
+ [slateblue1]Copying media tag [orange3]{0}[/] from secondary image[/]
+
\ No newline at end of file
diff --git a/Aaru/Commands/Image/Merge.cs b/Aaru/Commands/Image/Merge.cs
new file mode 100644
index 000000000..070666531
--- /dev/null
+++ b/Aaru/Commands/Image/Merge.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.Core;
+using Aaru.Core.Image;
+using Aaru.Localization;
+using Aaru.Logging;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace Aaru.Commands.Image;
+
+class MergeCommand : AsyncCommand
+{
+ const string MODULE_NAME = "Merge-image command";
+ static ProgressTask _progressTask1;
+ static ProgressTask _progressTask2;
+
+ public override async Task ExecuteAsync(CommandContext context, Settings settings,
+ CancellationToken cancellationToken)
+ {
+ MainClass.PrintCopyright();
+
+ // Initialize subchannel fix flags with cascading dependencies
+ bool fixSubchannel = settings.FixSubchannel;
+ bool fixSubchannelCrc = settings.FixSubchannelCrc;
+ bool fixSubchannelPosition = settings.FixSubchannelPosition;
+
+ if(fixSubchannelCrc) fixSubchannel = true;
+
+ if(fixSubchannel) fixSubchannelPosition = true;
+
+ Statistics.AddCommand("merge-image");
+
+ // Log all command parameters for debugging and auditing
+ Dictionary parsedOptions = Options.Parse(settings.Options);
+ LogCommandParameters(settings, fixSubchannelPosition, fixSubchannel, fixSubchannelCrc, parsedOptions);
+
+ var merger = new Merger(settings.PrimaryImagePath,
+ settings.SecondaryImagePath,
+ settings.OutputImagePath,
+ settings.UseSecondaryTags,
+ settings.SectorsFile,
+ settings.IgnoreMediaType,
+ settings.Comments,
+ settings.Count,
+ settings.Creator,
+ settings.DriveManufacturer,
+ settings.DriveModel,
+ settings.DriveFirmwareRevision,
+ settings.DriveSerialNumber,
+ settings.Format,
+ settings.MediaBarcode,
+ settings.LastMediaSequence,
+ settings.MediaManufacturer,
+ settings.MediaModel,
+ settings.MediaPartNumber,
+ settings.MediaSequence,
+ settings.MediaSerialNumber,
+ settings.MediaTitle,
+ parsedOptions,
+ settings.PrimaryResumeFile,
+ settings.SecondaryResumeFile,
+ settings.Geometry,
+ fixSubchannelPosition,
+ fixSubchannel,
+ fixSubchannelCrc,
+ settings.GenerateSubchannels,
+ settings.Decrypt,
+ settings.IgnoreNegativeSectors,
+ settings.IgnoreOverflowSectors);
+
+ ErrorNumber errno = ErrorNumber.NoError;
+
+ AnsiConsole.Progress()
+ .AutoClear(true)
+ .HideCompleted(true)
+ .Columns(new ProgressBarColumn(), new PercentageColumn(), new TaskDescriptionColumn())
+ .Start(ctx =>
+ {
+ merger.UpdateStatus += static text => AaruLogging.WriteLine(text);
+
+ merger.ErrorMessage += static text => AaruLogging.Error(text);
+
+ merger.StoppingErrorMessage += static text => AaruLogging.Error(text);
+
+ merger.UpdateProgress += (text, current, maximum) =>
+ {
+ _progressTask1 ??= ctx.AddTask("Progress");
+ _progressTask1.Description = text;
+ _progressTask1.Value = current;
+ _progressTask1.MaxValue = maximum;
+ };
+
+ merger.PulseProgress += text =>
+ {
+ if(_progressTask1 is null)
+ ctx.AddTask(text).IsIndeterminate();
+ else
+ {
+ _progressTask1.Description = text;
+ _progressTask1.IsIndeterminate = true;
+ }
+ };
+
+ merger.InitProgress += () => _progressTask1 = ctx.AddTask("Progress");
+
+ merger.EndProgress += static () =>
+ {
+ _progressTask1?.StopTask();
+ _progressTask1 = null;
+ };
+
+ merger.InitProgress2 += () => _progressTask2 = ctx.AddTask("Progress");
+
+ merger.EndProgress2 += static () =>
+ {
+ _progressTask2?.StopTask();
+ _progressTask2 = null;
+ };
+
+ merger.UpdateProgress2 += (text, current, maximum) =>
+ {
+ _progressTask2 ??= ctx.AddTask("Progress");
+ _progressTask2.Description = text;
+ _progressTask2.Value = current;
+ _progressTask2.MaxValue = maximum;
+ };
+
+ Console.CancelKeyPress += (_, e) =>
+ {
+ e.Cancel = true;
+ merger.Abort();
+ };
+
+ errno = merger.Start();
+ });
+
+ return (int)errno;
+ }
+
+ private static void LogCommandParameters(Settings settings, bool fixSubchannelPosition, bool fixSubchannel,
+ bool fixSubchannelCrc, Dictionary parsedOptions)
+ {
+ // Logs all command-line parameters for debugging and audit trail purposes
+
+ AaruLogging.Debug(MODULE_NAME, "--secondary-tags={0}", settings.UseSecondaryTags);
+ AaruLogging.Debug(MODULE_NAME, "--comments={0}", Markup.Escape(settings.Comments ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--count={0}", settings.Count);
+ AaruLogging.Debug(MODULE_NAME, "--creator={0}", Markup.Escape(settings.Creator ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--debug={0}", settings.Debug);
+
+ AaruLogging.Debug(MODULE_NAME, "--drive-manufacturer={0}", Markup.Escape(settings.DriveManufacturer ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--drive-model={0}", Markup.Escape(settings.DriveModel ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--drive-revision={0}", Markup.Escape(settings.DriveFirmwareRevision ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--drive-serial={0}", Markup.Escape(settings.DriveSerialNumber ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--ignore-media-type={0}", settings.IgnoreMediaType);
+ AaruLogging.Debug(MODULE_NAME, "--format={0}", Markup.Escape(settings.Format ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--geometry={0}", Markup.Escape(settings.Geometry ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--media-barcode={0}", Markup.Escape(settings.MediaBarcode ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--media-lastsequence={0}", settings.LastMediaSequence);
+
+ AaruLogging.Debug(MODULE_NAME, "--media-manufacturer={0}", Markup.Escape(settings.MediaManufacturer ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--media-model={0}", Markup.Escape(settings.MediaModel ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--media-partnumber={0}", Markup.Escape(settings.MediaPartNumber ?? ""));
+
+ AaruLogging.Debug(MODULE_NAME, "--media-sequence={0}", settings.MediaSequence);
+ AaruLogging.Debug(MODULE_NAME, "--media-serial={0}", Markup.Escape(settings.MediaSerialNumber ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--media-title={0}", Markup.Escape(settings.MediaTitle ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--options={0}", Markup.Escape(settings.Options ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--secondary-resume={0}", Markup.Escape(settings.SecondaryResumeFile ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--primary-resume={0}", Markup.Escape(settings.PrimaryResumeFile ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--verbose={0}", settings.Verbose);
+ AaruLogging.Debug(MODULE_NAME, "--fix-subchannel-position={0}", fixSubchannelPosition);
+ AaruLogging.Debug(MODULE_NAME, "--fix-subchannel={0}", fixSubchannel);
+ AaruLogging.Debug(MODULE_NAME, "--fix-subchannel-crc={0}", fixSubchannelCrc);
+ AaruLogging.Debug(MODULE_NAME, "--generate-subchannels={0}", settings.GenerateSubchannels);
+ AaruLogging.Debug(MODULE_NAME, "--decrypt={0}", settings.Decrypt);
+ AaruLogging.Debug(MODULE_NAME, "--sectors-file={0}", Markup.Escape(settings.SectorsFile ?? ""));
+ AaruLogging.Debug(MODULE_NAME, "--ignore-negative-sectors={0}", settings.IgnoreNegativeSectors);
+ AaruLogging.Debug(MODULE_NAME, "--ignore-overflow-sectors={0}", settings.IgnoreOverflowSectors);
+
+ AaruLogging.Debug(MODULE_NAME, UI.Parsed_options);
+
+ foreach(KeyValuePair parsedOption in parsedOptions)
+ AaruLogging.Debug(MODULE_NAME, "{0} = {1}", parsedOption.Key, parsedOption.Value);
+ }
+
+ public class Settings : ImageFamily
+ {
+ [LocalizedDescription(nameof(UI.Path_to_the_primary_image_file))]
+ [CommandArgument(0, "")]
+ public string PrimaryImagePath { get; init; }
+ [LocalizedDescription(nameof(UI.Path_to_the_secondary_image_file))]
+ [CommandArgument(1, "")]
+ public string SecondaryImagePath { get; init; }
+ [LocalizedDescription(nameof(UI.Path_to_the_output_merged_image_file))]
+ [CommandArgument(2, "")]
+ public string OutputImagePath { get; init; }
+ [LocalizedDescription(nameof(UI.Use_media_tags_from_secondary_image))]
+ [DefaultValue(false)]
+ [CommandOption("--secondary-tags")]
+ public bool UseSecondaryTags { get; init; }
+ [LocalizedDescription(nameof(UI.File_containing_list_of_sectors_to_take_from_secondary_image))]
+ [DefaultValue(null)]
+ [CommandOption("--sectors-file")]
+ public string SectorsFile { get; init; }
+ [LocalizedDescription(nameof(UI.Ignore_mismatched_image_media_type))]
+ [DefaultValue(false)]
+ [CommandOption("--ignore-media-type")]
+ public bool IgnoreMediaType { get; init; }
+ [LocalizedDescription(nameof(UI.Image_comments))]
+ [DefaultValue(null)]
+ [CommandOption("--comments")]
+ public string Comments { get; init; }
+ [LocalizedDescription(nameof(UI.How_many_sectors_to_convert_at_once))]
+ [DefaultValue(64)]
+ [CommandOption("-c|--count")]
+ public int Count { get; init; }
+ [LocalizedDescription(nameof(UI.Who_person_created_the_image))]
+ [DefaultValue(null)]
+ [CommandOption("--creator")]
+ public string Creator { get; init; }
+ [LocalizedDescription(nameof(UI.Manufacturer_of_drive_read_the_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--drive-manufacturer")]
+ public string DriveManufacturer { get; init; }
+ [LocalizedDescription(nameof(UI.Model_of_drive_used_by_media))]
+ [DefaultValue(null)]
+ [CommandOption("--drive-model")]
+ public string DriveModel { get; init; }
+ [LocalizedDescription(nameof(UI.Firmware_revision_of_drive_read_the_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--drive-revision")]
+ public string DriveFirmwareRevision { get; init; }
+ [LocalizedDescription(nameof(UI.Serial_number_of_drive_read_the_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--drive-serial")]
+ public string DriveSerialNumber { get; init; }
+ [LocalizedDescription(nameof(UI.Format_of_the_output_image_as_plugin_name_or_plugin_id))]
+ [DefaultValue(null)]
+ [CommandOption("-p|--format")]
+ public string Format { get; init; }
+ [LocalizedDescription(nameof(UI.Barcode_of_the_media))]
+ [DefaultValue(null)]
+ [CommandOption("--media-barcode")]
+ public string MediaBarcode { get; init; }
+ [LocalizedDescription(nameof(UI.Last_media_of_sequence_by_image))]
+ [DefaultValue(0)]
+ [CommandOption("--media-lastsequence")]
+ public int LastMediaSequence { get; init; }
+ [LocalizedDescription(nameof(UI.Manufacturer_of_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--media-manufacturer")]
+ public string MediaManufacturer { get; init; }
+ [LocalizedDescription(nameof(UI.Model_of_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--media-model")]
+ public string MediaModel { get; init; }
+ [LocalizedDescription(nameof(UI.Part_number_of_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--media-partnumber")]
+ public string MediaPartNumber { get; init; }
+ [LocalizedDescription(nameof(UI.Number_in_sequence_for_media_by_image))]
+ [DefaultValue(0)]
+ [CommandOption("--media-sequence")]
+ public int MediaSequence { get; init; }
+ [LocalizedDescription(nameof(UI.Serial_number_of_media_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--media-serial")]
+ public string MediaSerialNumber { get; init; }
+ [LocalizedDescription(nameof(UI.Title_of_media_represented_by_image))]
+ [DefaultValue(null)]
+ [CommandOption("--media-title")]
+ public string MediaTitle { get; init; }
+ [LocalizedDescription(nameof(UI.Comma_separated_name_value_pairs_of_image_options))]
+ [DefaultValue(null)]
+ [CommandOption("-O|--options")]
+ public string Options { get; init; }
+ [LocalizedDescription(nameof(UI.Resume_file_for_primary_image))]
+ [DefaultValue(null)]
+ [CommandOption("--primary-resume")]
+ public string PrimaryResumeFile { get; init; }
+ [LocalizedDescription(nameof(UI.Resume_file_for_secondary_image))]
+ [DefaultValue(null)]
+ [CommandOption("--secondary-resume")]
+ public string SecondaryResumeFile { get; init; }
+ [LocalizedDescription(nameof(UI.Force_geometry_help))]
+ [DefaultValue(null)]
+ [CommandOption("-g|--geometry")]
+ public string Geometry { get; init; }
+ [LocalizedDescription(nameof(UI.Fix_subchannel_position_help))]
+ [DefaultValue(true)]
+ [CommandOption("--fix-subchannel-position")]
+ public bool FixSubchannelPosition { get; init; }
+ [LocalizedDescription(nameof(UI.Fix_subchannel_help))]
+ [DefaultValue(false)]
+ [CommandOption("--fix-subchannel")]
+ public bool FixSubchannel { get; init; }
+ [LocalizedDescription(nameof(UI.Fix_subchannel_crc_help))]
+ [DefaultValue(false)]
+ [CommandOption("--fix-subchannel-crc")]
+ public bool FixSubchannelCrc { get; init; }
+ [LocalizedDescription(nameof(UI.Generates_subchannels_help))]
+ [DefaultValue(false)]
+ [CommandOption("--generate-subchannels")]
+ public bool GenerateSubchannels { get; init; }
+ [LocalizedDescription(nameof(UI.Decrypt_sectors_help))]
+ [DefaultValue(false)]
+ [CommandOption("--decrypt")]
+ public bool Decrypt { get; init; }
+ [LocalizedDescription(nameof(UI.Ignore_negative_sectors))]
+ [DefaultValue(false)]
+ [CommandOption("--ignore-negative-sectors")]
+ public bool IgnoreNegativeSectors { get; init; }
+ [LocalizedDescription(nameof(UI.Ignore_overflow_sectors))]
+ [DefaultValue(false)]
+ [CommandOption("--ignore-overflow-sectors")]
+ public bool IgnoreOverflowSectors { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/Aaru/Main.cs b/Aaru/Main.cs
index 287926701..bd0a6823d 100644
--- a/Aaru/Main.cs
+++ b/Aaru/Main.cs
@@ -399,6 +399,9 @@ class MainClass
image.AddCommand("verify")
.WithAlias("v")
.WithDescription(UI.Image_Verify_Command_Description);
+
+ image.AddCommand("merge")
+ .WithDescription(UI.Image_Merge_Command_Description);
})
.WithAlias("i")
.WithAlias("img");