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