Implement a command to merge two images into a combined third image.

This commit is contained in:
2025-12-21 20:04:50 +00:00
parent e7066b798f
commit c6db847b6a
24 changed files with 4157 additions and 12 deletions

View File

@@ -1,13 +1,10 @@
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml:space="preserve">
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Ccompactdisc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Clinearmemory/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Cplaystationportable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Csbc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=sidecar/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Ccompactdisc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Clinearmemory/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Cplaystationportable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=devices_005Cdumping_005Csbc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=image_005Cmerge/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=sidecar/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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<ulong> CalculateSectorsToCopy(IMediaImage primaryImage, IMediaImage secondaryImage, Resume primaryResume,
Resume secondaryResume, List<ulong> overrideSectorsList)
{
List<DumpHardware> primaryTries = (primaryResume != null ? primaryResume.Tries : primaryImage.DumpHardware) ??
[
new DumpHardware
{
Extents =
[
new Extent
{
Start = 0,
End = primaryImage.Info.Sectors - 1
}
]
}
];
List<DumpHardware> 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<ulong>();
// 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<DumpHardware> CalculateMergedDumpHardware(IMediaImage primaryImage, IMediaImage secondaryImage,
Resume primaryResume, Resume secondaryResume,
List<ulong> overrideSectorsList)
{
List<DumpHardware> primaryTries = (primaryResume != null ? primaryResume.Tries : primaryImage.DumpHardware) ??
[
new DumpHardware
{
Extents =
[
new Extent
{
Start = 0,
End = primaryImage.Info.Sectors - 1
}
]
}
];
List<DumpHardware> secondaryTries =
(secondaryResume != null ? secondaryResume.Tries : secondaryImage.DumpHardware) ??
[
new DumpHardware
{
Extents =
[
new Extent
{
Start = 0,
End = secondaryImage.Info.Sectors - 1
}
]
}
];
var mergedHardware = new List<DumpHardware>();
// Create a mapping of which hardware each sector belongs to
var sectorToHardware = new Dictionary<ulong, DumpHardware>();
// First, build a lookup of which hardware each sector belongs to in primary tries
var primarySectorToHardware = new Dictionary<ulong, DumpHardware>();
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<ulong, DumpHardware>();
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<DumpHardware> 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
}
]
});
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<uint> overrideNegativeSectors)
{
if(_aborted) return ErrorNumber.NoError;
ErrorNumber errno = ErrorNumber.NoError;
InitProgress?.Invoke();
List<uint> 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<uint> overrideNegativeSectors)
{
if(_aborted) return ErrorNumber.NoError;
ErrorNumber errno = ErrorNumber.NoError;
InitProgress?.Invoke();
List<uint> 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<ulong> overrideOverflowSectors)
{
if(_aborted) return ErrorNumber.NoError;
ErrorNumber errno = ErrorNumber.NoError;
InitProgress?.Invoke();
List<uint> 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<ulong> overrideOverflowSectors)
{
if(_aborted) return ErrorNumber.NoError;
ErrorNumber errno = ErrorNumber.NoError;
InitProgress?.Invoke();
List<ulong> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<IBaseWritableImage> 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;
}
}

View File

@@ -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<string, string> options,
string primaryResumeFile,
string secondaryResumeFile,
string geometry,
bool fixSubchannelPosition,
bool fixSubchannel,
bool fixSubchannelCrc,
bool generateSubchannels,
bool decrypt,
bool ignoreNegativeSectors,
bool ignoreOverflowSectors
)
{
const string MODULE_NAME = "Image merger";
bool _aborted;
readonly PluginRegister _plugins = PluginRegister.Singleton;
public ErrorNumber Start()
{
// Validate sector count parameter
if(count == 0)
{
StoppingErrorMessage?.Invoke(UI.Need_to_specify_more_than_zero_sectors_to_copy_at_once);
return ErrorNumber.InvalidArgument;
}
// Parse and validate CHS geometry if specified
(bool success, uint cylinders, uint heads, uint sectors)? geometryResult = ParseGeometry(geometry);
(uint cylinders, uint heads, uint sectors)? geometryValues = null;
if(geometryResult is not null)
{
if(!geometryResult.Value.success) return ErrorNumber.InvalidArgument;
geometryValues = (geometryResult.Value.cylinders, geometryResult.Value.heads, geometryResult.Value.sectors);
}
// Load resume information from sidecar files
(bool success, Resume primaryResume) = LoadMetadata(primaryResumeFile);
if(!success) return ErrorNumber.InvalidArgument;
(success, Resume secondaryResume) = LoadMetadata(secondaryResumeFile);
if(!success) return ErrorNumber.InvalidArgument;
// Verify output file doesn't already exist
if(File.Exists(outputImagePath))
{
StoppingErrorMessage?.Invoke(UI.Output_file_already_exists);
return ErrorNumber.FileExists;
}
(ErrorNumber errno, IMediaImage primaryImage) = GetInputImage(primaryImagePath);
if(primaryImage is null) return errno;
(errno, IMediaImage secondaryImage) = GetInputImage(secondaryImagePath);
if(secondaryImage is null) return errno;
// Get media type and handle obsolete type mappings for backwards compatibility
MediaType primaryMediaType = primaryImage.Info.MediaType;
// Obsolete types
#pragma warning disable 612
primaryMediaType = primaryMediaType switch
{
MediaType.SQ1500 => MediaType.SyJet,
MediaType.Bernoulli => MediaType.Bernoulli10,
MediaType.Bernoulli2 => MediaType.BernoulliBox2_20,
_ => primaryImage.Info.MediaType
};
#pragma warning restore 612
MediaType secondaryMediaType = secondaryImage.Info.MediaType;
// Obsolete types
#pragma warning disable 612
secondaryMediaType = secondaryMediaType switch
{
MediaType.SQ1500 => MediaType.SyJet,
MediaType.Bernoulli => MediaType.Bernoulli10,
MediaType.Bernoulli2 => MediaType.BernoulliBox2_20,
_ => secondaryImage.Info.MediaType
};
#pragma warning restore 612
if(!ignoreMediaType && primaryMediaType != secondaryMediaType)
{
StoppingErrorMessage?.Invoke(UI.Images_have_different_media_types_cannot_merge);
return ErrorNumber.InvalidArgument;
}
// Discover and load output format plugin
IWritableImage outputFormat = FindOutputFormat(PluginRegister.Singleton, format, outputImagePath);
if(outputFormat == null) return ErrorNumber.FormatNotFound;
UpdateStatus?.Invoke(string.Format(UI.Output_image_format_0, outputFormat.Name));
if(primaryImage.Info.Sectors != secondaryImage.Info.Sectors)
{
StoppingErrorMessage?.Invoke(UI.Images_have_different_number_of_sectors_cannot_merge);
return ErrorNumber.InvalidArgument;
}
errno = ValidateMediaCapabilities(primaryImage, secondaryImage, outputFormat, primaryMediaType);
if(errno != ErrorNumber.NoError) return errno;
// Validate sector tags compatibility between formats
errno = ValidateSectorTags(primaryImage, outputFormat, out bool useLong);
if(errno != ErrorNumber.NoError) return errno;
// Check and setup tape image support if needed
var primaryTape = primaryImage as ITapeImage;
var secondaryTape = secondaryImage as ITapeImage;
var outputTape = outputFormat as IWritableTapeImage;
errno = ValidateTapeImage(primaryTape, secondaryTape, outputTape);
if(errno != ErrorNumber.NoError) return errno;
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Parsing_sectors_file);
(List<ulong> overrideSectorsList, List<uint> overrideNegativeSectorsList) =
ParseOverrideSectorsList(sectorsFile);
EndProgress?.Invoke();
if(overrideNegativeSectorsList.Contains(0))
{
StoppingErrorMessage?.Invoke(UI.Sectors_file_contains_invalid_sector_number_0_not_continuing);
return ErrorNumber.InvalidArgument;
}
uint nominalNegativeSectors = 0;
uint nominalOverflowSectors = 0;
if(!ignoreNegativeSectors)
{
nominalNegativeSectors = primaryImage.Info.NegativeSectors;
if(secondaryImage.Info.NegativeSectors > nominalNegativeSectors)
nominalNegativeSectors = secondaryImage.Info.NegativeSectors;
}
if(!ignoreOverflowSectors)
{
nominalOverflowSectors = primaryImage.Info.OverflowSectors;
if(secondaryImage.Info.OverflowSectors > nominalOverflowSectors)
nominalOverflowSectors = secondaryImage.Info.OverflowSectors;
}
// Check if any override sectors is bigger than the biggest overflow sector
ulong maxAllowedSector = primaryImage.Info.Sectors - 1 + nominalOverflowSectors;
if(overrideSectorsList.Count > 0 && overrideSectorsList.Max() > maxAllowedSector)
{
StoppingErrorMessage
?.Invoke(string.Format(UI
.Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_continuing,
overrideSectorsList.Max(),
maxAllowedSector));
return ErrorNumber.InvalidArgument;
}
InitProgress?.Invoke();
PulseProgress?.Invoke(UI.Calculating_sectors_to_merge);
List<ulong> sectorsToCopyFromSecondImage =
CalculateSectorsToCopy(primaryImage, secondaryImage, primaryResume, secondaryResume, overrideSectorsList);
EndProgress?.Invoke();
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> 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;
}
/// <summary>Event raised when the progress bar is no longer needed</summary>
public event EndProgressHandler EndProgress;
/// <summary>Event raised when a progress bar is needed</summary>
public event InitProgressHandler InitProgress;
/// <summary>Event raised to report status updates</summary>
public event UpdateStatusHandler UpdateStatus;
/// <summary>Event raised to report a non-fatal error</summary>
public event ErrorMessageHandler ErrorMessage;
/// <summary>Event raised to report a fatal error that stops the dumping operation and should call user's attention</summary>
public event ErrorMessageHandler StoppingErrorMessage;
/// <summary>Event raised to update the values of a determinate progress bar</summary>
public event UpdateProgressHandler UpdateProgress;
/// <summary>Event raised to update the status of an indeterminate progress bar</summary>
public event PulseProgressHandler PulseProgress;
/// <summary>Event raised when the progress bar is no longer needed</summary>
public event EndProgressHandler2 EndProgress2;
/// <summary>Event raised when a progress bar is needed</summary>
public event InitProgressHandler2 InitProgress2;
/// <summary>Event raised to update the values of a determinate progress bar</summary>
public event UpdateProgressHandler2 UpdateProgress2;
public void Abort()
{
_aborted = true;
}
}

View File

@@ -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;
}
}

View File

@@ -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<ulong> 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<byte, string> isrcs = [];
Dictionary<byte, byte> trackFlags = [];
string mcn = null;
HashSet<int> subchannelExtents = [];
Dictionary<byte, int> 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<ushort, int> 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<byte, string> 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<ulong> 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<byte, string> isrcs, ref string mcn,
Track[] tracks, HashSet<int> subchannelExtents,
Dictionary<byte, int> 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<byte, string> isrcs, ref string mcn,
Track[] tracks, HashSet<int> subchannelExtents,
Dictionary<byte, int> smallestPregapLbaPerTrack,
List<ulong> 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;
}
/// <summary>
/// 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
/// </summary>
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);
}
}
}
}
/// <summary>
/// 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
/// </summary>
void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, ref byte[] generatedTitleKeys)
{
if(_aborted) return;
List<Partition> partitions = Partitions.GetAll(inputOptical);
partitions = partitions.FindAll(p =>
{
Filesystems.Identify(inputOptical, out List<string> 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;
}

View File

@@ -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<ulong> overrideSectors, List<uint> overrideNegativeSectors) ParseOverrideSectorsList(
string overrideSectorsListPath)
{
if(overrideSectorsListPath is null) return ([], []);
List<ulong> overrideSectors = [];
List<uint> 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);
}
}

View File

@@ -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<ulong> 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<ulong> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -3499,4 +3499,7 @@ No tiene sentido hacerlo y supondría demasiado esfuerzo para la cinta.</value>
<data name="DVD_2nd_layer_Physical_Format_Information_contained_in_image_WithMarkup" xml:space="preserve">
<value>[bold][blue]Información del formato físico del DVD (2ª capa) contenida en la imagen:[/][/]</value>
</data>
<data name="Media_tag_0_present_in_primary_image_will_be_lost" xml:space="preserve">
<value>[red]La etiqueta del medio {0} presente en la imagen primaria se perderá en el formato de salida. No se continuará...[/]</value>
</data>
</root>

View File

@@ -3520,4 +3520,7 @@ It has no sense to do it, and it will put too much strain on the tape.</value>
<data name="DVD_2nd_layer_Physical_Format_Information_contained_in_image_WithMarkup" xml:space="preserve">
<value>[bold][blue]DVD 2nd layer Physical Format Information contained in image:[/][/]</value>
</data>
<data name="Media_tag_0_present_in_primary_image_will_be_lost" xml:space="preserve">
<value>[red]Media tag {0} present in primary image will be lost in output format. Not continuing...[/]</value>
</data>
</root>

View File

@@ -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);
}
}
}
}

View File

@@ -5255,4 +5255,138 @@ Probadores:
<data name="Use_long_sectors" xml:space="preserve">
<value>Usar sectores largos (con etiquetas).</value>
</data>
<data name="Image_Merge_Command_Description" xml:space="preserve">
<value>Combina dos imágenes en una</value>
</data>
<data name="Path_to_the_primary_image_file" xml:space="preserve">
<value>Ruta al archivo de imagen primario.</value>
</data>
<data name="Path_to_the_secondary_image_file" xml:space="preserve">
<value>Ruta al archivo de imagen secundario.</value>
</data>
<data name="Path_to_the_output_merged_image_file" xml:space="preserve">
<value>Ruta al archivo de imagen combinado.</value>
</data>
<data name="Use_media_tags_from_secondary_image" xml:space="preserve">
<value>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).</value>
</data>
<data name="File_containing_list_of_sectors_to_take_from_secondary_image" xml:space="preserve">
<value>Archivo que contiene la lista de sectores a obtener de la imagen secundaria (un sector por línea).</value>
</data>
<data name="Ignore_mismatched_image_media_type" xml:space="preserve">
<value>Ignorar los tipos de medio no coincidentes. La imagen combinada tendrá el tipo de medio de la imagen primaria.</value>
</data>
<data name="Resume_file_for_primary_image" xml:space="preserve">
<value>Fichero de resumen para la imagen primaria</value>
</data>
<data name="Resume_file_for_secondary_image" xml:space="preserve">
<value>Fichero de resumen para la imagen secundaria</value>
</data>
<data name="Media_tag_0_present_in_primary_image_will_be_lost_in_output_format" xml:space="preserve">
<value>[red]La etiqueta del medio [orange3]{0}[/] presente en la imagen primaria se perderá en el formato de salida. No se continuará...[/]</value>
</data>
<data name="Media_tag_0_present_in_secondary_image_will_be_lost_in_output_format" xml:space="preserve">
<value>[red]La etiqueta del medio [orange3]{0}[/] presente en la imagen secundaria se perderá en el formato de salida. No se continuará...[/]</value>
</data>
<data name="Output_image_does_not_support_sector_tag_0_data_will_be_lost" xml:space="preserve">
<value>[red]La imagen de salida no soporta la etiqueta de sector [orange3]{0}[/], se perderán datos. No se continuará...[/]</value>
</data>
<data name="Copying_negative_sector_0_of_1" xml:space="preserve">
<value>[slateblue1]Copiando sector [lime]-{0}[/] de [violet]-{1}[/][/]</value>
</data>
<data name="Copying_tag_1_for_negative_sector_0" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta [orange3]{1}[/] para el sector [lime]-{0}[/][/]</value>
</data>
<data name="Copying_negative_sector_0" xml:space="preserve">
<value>[slateblue1]Copiando sector [lime]-{0}[/][/]</value>
</data>
<data name="Copying_overflow_sector_0_of_1" xml:space="preserve">
<value>[slateblue1]Copiando sector de sobrecarga [lime]{0}[/] de [violet]{1}[/][/]</value>
</data>
<data name="Copying_tag_1_for_overflow_sector_0" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta [orange3]{1}[/] para el sector de sobrecarga [lime]{0}[/][/]</value>
</data>
<data name="Copying_overflow_sector_0" xml:space="preserve">
<value>[slateblue1]Copiando sector de sobrecarga [lime]{0}[/][/]</value>
</data>
<data name="Images_have_different_media_types_cannot_merge" xml:space="preserve">
<value>[red]Las imágenes tienen diferentes tipos de medio, no se pueden combinar.[/]</value>
</data>
<data name="Images_have_different_number_of_sectors_cannot_merge" xml:space="preserve">
<value>[red]Las imágenes tienen diferente número de sectores, no se pueden combinar.[/]</value>
</data>
<data name="Parsing_sectors_file" xml:space="preserve">
<value>[slateblue1]Interpretando el fichero de sectores...[/]</value>
</data>
<data name="Sectors_file_contains_invalid_sector_number_0_not_continuing" xml:space="preserve">
<value>[red]El fichero de sectores contiene el sector inválido 0, no se continuará...[/]</value>
</data>
<data name="Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_continuing"
xml:space="preserve">
<value>[red]El fichero de sectores contiene el sector [lime]{0}[/] que excede el sector máximo permitido [violet]{1}[/], no se continuará...[/]</value>
</data>
<data name="Calculating_sectors_to_merge" xml:space="preserve">
<value>[slateblue1]Calculando sectores a combinar...[/]</value>
</data>
<data name="No_sectors_to_merge__output_image_will_be_identical_to_primary_image_not_continuing"
xml:space="preserve">
<value>[red]No hay sectores a combinar, la imagen de salida sería idéntica a la primaria, no se continuará...[/]</value>
</data>
<data name="Calculating_merged_dump_hardware_list" xml:space="preserve">
<value>[slateblue1]Calculando list de hardware de volcado combinada...[/]</value>
</data>
<data name="Copying_0_sectors_from_primary_image" xml:space="preserve">
<value>[slateblue1]Copiando [teal]{0}[/] sectores de la imagen primaria[/]</value>
</data>
<data name="Will_copy_0_sectors_from_secondary_image" xml:space="preserve">
<value>[slateblue1]Se copiarán [teal]{0}[/] sectores de la imagen secundaria.[/]</value>
</data>
<data name="Flux_data_will_be_copied_as_is_from_primary_image" xml:space="preserve">
<value>[slateblue1]Los datos de flujo se copiarán tal cual de la imagen primaria...[/]</value>
</data>
<data name="Copying_file_0_of_partition_1" xml:space="preserve">
<value>[slateblue1]Copiando fichero [lime]{0}[/] de partición [teal]{1}[/]...[/]</value>
</data>
<data name="Copying_tape_partition_0" xml:space="preserve">
<value>[slateblue1]Copiando partición de cinta [teal]{0}[/]...[/]</value>
</data>
<data name="Merge_completed_successfully" xml:space="preserve">
<value>[green]¡Combinación completada con éxito![/]</value>
</data>
<data name="Decrypting_encrypted_sectors" xml:space="preserve">
<value>[green]Desencriptando sectores encriptados.[/]</value>
</data>
<data name="Copying_sectors_in_track_0_of_1" xml:space="preserve">
<value>[slateblue1]Copiando sectores en pista [teal]{0}[/] de [teal]{1}[/][/]</value>
</data>
<data name="Copying_sectors_0_to_1_in_track_2" xml:space="preserve">
<value>[slateblue1]Copiando sectores del [lime]{0}[/] al [violet]{1}[/] en la pista [teal]{2}[/][/]</value>
</data>
<data name="Input_image_is_not_returning_long_sectors_not_continuing" xml:space="preserve">
<value>[red]El formato de imagen de entrada no está devolviendo sectores sin procesar, no se continuará...[/]</value>
</data>
<data name="Copying_sector_0" xml:space="preserve">
<value>[slateblue1]Copiando sector [lime]{0}[/][/]</value>
</data>
<data name="Copying_tags_in_track_0_of_1" xml:space="preserve">
<value>[slateblue1]Copiando etiquetas en la pista [teal]{0}[/] de [teal]{1}[/][/]</value>
</data>
<data name="Copying_tag_3_for_sectors_0_to_1_in_track_2" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta [orange3]{3}[/] para los sectores del [lime]{0}[/] al [violet]{1}[/] en la pista [teal]{2}[/][/]</value>
</data>
<data name="Copying_tag_0_for_sector_1" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta [orange3]{0}[/] para el sector [lime]{1}[/][/]</value>
</data>
<data name="Copying_sectors_0_to_1" xml:space="preserve">
<value>[slateblue1]Copiando sectores del [lime]{0}[/] al [violet]{1}[/][/]</value>
</data>
<data name="Copying_tag_2_for_sectors_0_to_1" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta [orange3]{2}[/] para los sectores del [lime]{0}[/] al [violet]{1}[/][/]</value>
</data>
<data name="Copying_media_tag_0_from_primary_image" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta del medio [orange3]{0}[/] de la imagen primaria[/]</value>
</data>
<data name="Copying_media_tag_0_from_secondary_image" xml:space="preserve">
<value>[slateblue1]Copiando etiqueta del medio [orange3]{0}[/] de la imagen secundaria[/]</value>
</data>
</root>

View File

@@ -5333,4 +5333,138 @@ Do you want to continue?</value>
<data name="Use_long_sectors" xml:space="preserve">
<value>Use long sectors (with tags).</value>
</data>
<data name="Image_Merge_Command_Description" xml:space="preserve">
<value>Merges two images into one</value>
</data>
<data name="Path_to_the_primary_image_file" xml:space="preserve">
<value>Path to the primary image file.</value>
</data>
<data name="Path_to_the_secondary_image_file" xml:space="preserve">
<value>Path to the secondary image file.</value>
</data>
<data name="Path_to_the_output_merged_image_file" xml:space="preserve">
<value>Path to the output merged image file.</value>
</data>
<data name="Use_media_tags_from_secondary_image" xml:space="preserve">
<value>Use media tags from secondary image (otherwise when in both images, primary image tags are used).</value>
</data>
<data name="File_containing_list_of_sectors_to_take_from_secondary_image" xml:space="preserve">
<value>File containing list of sectors to take from secondary image (one sector number per line).</value>
</data>
<data name="Ignore_mismatched_image_media_type" xml:space="preserve">
<value>Ignore mismatched image media type. Merged image will still have primary image media type.</value>
</data>
<data name="Resume_file_for_primary_image" xml:space="preserve">
<value>Resume file for primary image</value>
</data>
<data name="Resume_file_for_secondary_image" xml:space="preserve">
<value>Resume file for secondary image</value>
</data>
<data name="Media_tag_0_present_in_primary_image_will_be_lost_in_output_format" xml:space="preserve">
<value>[red]Media tag [orange3]{0}[/] present in primary image will be lost in output format. Not continuing...[/]</value>
</data>
<data name="Media_tag_0_present_in_secondary_image_will_be_lost_in_output_format" xml:space="preserve">
<value>[red]Media tag [orange3]{0}[/] present in secondary image will be lost in output format. Not continuing...[/]</value>
</data>
<data name="Output_image_does_not_support_sector_tag_0_data_will_be_lost" xml:space="preserve">
<value>[red]Output image does not support sector tag [orange3]{0}[/], data will be lost. Not continuing...[/]</value>
</data>
<data name="Copying_negative_sector_0_of_1" xml:space="preserve">
<value>[slateblue1]Copying sector [lime]-{0}[/] of [violet]-{1}[/][/]</value>
</data>
<data name="Copying_tag_1_for_negative_sector_0" xml:space="preserve">
<value>[slateblue1]Copying tag [orange3]{1}[/] for sector [lime]-{0}[/][/]</value>
</data>
<data name="Copying_negative_sector_0" xml:space="preserve">
<value>[slateblue1]Copying sector [lime]-{0}[/][/]</value>
</data>
<data name="Copying_overflow_sector_0_of_1" xml:space="preserve">
<value>[slateblue1]Copying overflow sector [lime]{0}[/] of [violet]{1}[/][/]</value>
</data>
<data name="Copying_tag_1_for_overflow_sector_0" xml:space="preserve">
<value>[slateblue1]Copying tag [orange3]{1}[/] for overflow sector [lime]{0}[/][/]</value>
</data>
<data name="Copying_overflow_sector_0" xml:space="preserve">
<value>[slateblue1]Copying overflow sector [lime]{0}[/][/]</value>
</data>
<data name="Images_have_different_media_types_cannot_merge" xml:space="preserve">
<value>[red]Images have different media types, cannot merge.[/]</value>
</data>
<data name="Images_have_different_number_of_sectors_cannot_merge" xml:space="preserve">
<value>[red]Images have different number of sectors, cannot merge.[/]</value>
</data>
<data name="Parsing_sectors_file" xml:space="preserve">
<value>[slateblue1]Parsing sectors file...[/]</value>
</data>
<data name="Sectors_file_contains_invalid_sector_number_0_not_continuing" xml:space="preserve">
<value>[red]Sectors file contains invalid sector number 0, not continuing...[/]</value>
</data>
<data name="Sectors_file_contains_sector_0_which_exceeds_the_maximum_allowed_sector_1_not_continuing"
xml:space="preserve">
<value>[red]Sectors file contains sector [lime]{0}[/] which exceeds the maximum allowed sector [violet]{1}[/], not continuing...[/]</value>
</data>
<data name="Calculating_sectors_to_merge" xml:space="preserve">
<value>[slateblue1]Calculating sectors to merge...[/]</value>
</data>
<data name="No_sectors_to_merge__output_image_will_be_identical_to_primary_image_not_continuing"
xml:space="preserve">
<value>[red]No sectors to merge, output image will be identical to primary image, not continuing...[/]</value>
</data>
<data name="Calculating_merged_dump_hardware_list" xml:space="preserve">
<value>[slateblue1]Calculating merged dump hardware list...[/]</value>
</data>
<data name="Copying_0_sectors_from_primary_image" xml:space="preserve">
<value>[slateblue1]Copying [teal]{0}[/] sectors from primary image[/]</value>
</data>
<data name="Will_copy_0_sectors_from_secondary_image" xml:space="preserve">
<value>[slateblue1]Will copy [teal]{0}[/] sectors from secondary image.[/]</value>
</data>
<data name="Flux_data_will_be_copied_as_is_from_primary_image" xml:space="preserve">
<value>[slateblue1]Flux data will be copied as-is from primary image...[/]</value>
</data>
<data name="Copying_file_0_of_partition_1" xml:space="preserve">
<value>[slateblue1]Copying file [lime]{0}[/] of partition [teal]{1}[/]...[/]</value>
</data>
<data name="Copying_tape_partition_0" xml:space="preserve">
<value>[slateblue1]Copying tape partition [teal]{0}[/]...[/]</value>
</data>
<data name="Merge_completed_successfully" xml:space="preserve">
<value>[green]Merge completed successfully![/]</value>
</data>
<data name="Decrypting_encrypted_sectors" xml:space="preserve">
<value>[green]Decrypting encrypted sectors.[/]</value>
</data>
<data name="Copying_sectors_in_track_0_of_1" xml:space="preserve">
<value>[slateblue1]Copying sectors in track [teal]{0}[/] of [teal]{1}[/][/]</value>
</data>
<data name="Copying_sectors_0_to_1_in_track_2" xml:space="preserve">
<value>[slateblue1]Copying sectors [lime]{0}[/] to [violet]{1}[/] in track [teal]{2}[/][/]</value>
</data>
<data name="Input_image_is_not_returning_long_sectors_not_continuing" xml:space="preserve">
<value>[red]Input image is not returning raw sectors, not continuing...[/]</value>
</data>
<data name="Copying_sector_0" xml:space="preserve">
<value>[slateblue1]Copying sector [lime]{0}[/][/]</value>
</data>
<data name="Copying_tags_in_track_0_of_1" xml:space="preserve">
<value>[slateblue1]Copying tags in track [teal]{0}[/] of [teal]{1}[/][/]</value>
</data>
<data name="Copying_tag_3_for_sectors_0_to_1_in_track_2" xml:space="preserve">
<value>[slateblue1]Copying tag [orange3]{3}[/] for sectors [lime]{0}[/] to [violet]{1}[/] in track [teal]{2}[/][/]</value>
</data>
<data name="Copying_tag_0_for_sector_1" xml:space="preserve">
<value>[slateblue1]Copying tag [orange3]{0}[/] for sector [lime]{1}[/][/]</value>
</data>
<data name="Copying_sectors_0_to_1" xml:space="preserve">
<value>[slateblue1]Copying sectors [lime]{0}[/] to [violet]{1}[/][/]</value>
</data>
<data name="Copying_tag_2_for_sectors_0_to_1" xml:space="preserve">
<value>[slateblue1]Copying tag [orange3]{2}[/] for sectors [lime]{0}[/] to [violet]{1}[/][/]</value>
</data>
<data name="Copying_media_tag_0_from_primary_image" xml:space="preserve">
<value>[slateblue1]Copying media tag [orange3]{0}[/] from primary image[/]</value>
</data>
<data name="Copying_media_tag_0_from_secondary_image" xml:space="preserve">
<value>[slateblue1]Copying media tag [orange3]{0}[/] from secondary image[/]</value>
</data>
</root>

View File

@@ -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<MergeCommand.Settings>
{
const string MODULE_NAME = "Merge-image command";
static ProgressTask _progressTask1;
static ProgressTask _progressTask2;
public override async Task<int> 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<string, string> 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<string, string> 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<string, string> 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, "<primary-image>")]
public string PrimaryImagePath { get; init; }
[LocalizedDescription(nameof(UI.Path_to_the_secondary_image_file))]
[CommandArgument(1, "<secondary-image>")]
public string SecondaryImagePath { get; init; }
[LocalizedDescription(nameof(UI.Path_to_the_output_merged_image_file))]
[CommandArgument(2, "<output-image>")]
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; }
}
}

View File

@@ -399,6 +399,9 @@ class MainClass
image.AddCommand<VerifyCommand>("verify")
.WithAlias("v")
.WithDescription(UI.Image_Verify_Command_Description);
image.AddCommand<MergeCommand>("merge")
.WithDescription(UI.Image_Merge_Command_Description);
})
.WithAlias("i")
.WithAlias("img");