Files
Aaru/Aaru/Commands/Image/Compare.cs

547 lines
23 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Compare.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Commands.
//
// --[ Description ] ----------------------------------------------------------
//
// Implements the 'compare' command.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Core;
using Aaru.Helpers;
using Aaru.Localization;
using Aaru.Logging;
using Sentry;
using Spectre.Console;
using Spectre.Console.Cli;
using ImageInfo = Aaru.CommonTypes.Structs.ImageInfo;
namespace Aaru.Commands.Image;
sealed class CompareCommand : Command<CompareCommand.Settings>
{
const string MODULE_NAME = "Compare command";
public override int Execute(CommandContext context, Settings settings)
{
MainClass.PrintCopyright();
Statistics.AddCommand("compare");
AaruLogging.Debug(MODULE_NAME, "--debug={0}", settings.Debug);
AaruLogging.Debug(MODULE_NAME, "--input1={0}", Markup.Escape(settings.ImagePath1 ?? ""));
AaruLogging.Debug(MODULE_NAME, "--input2={0}", Markup.Escape(settings.ImagePath2 ?? ""));
AaruLogging.Debug(MODULE_NAME, "--verbose={0}", settings.Verbose);
IFilter inputFilter1 = null;
IFilter inputFilter2 = null;
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Identifying_first_file_filter).IsIndeterminate();
inputFilter1 = PluginRegister.Singleton.GetFilter(settings.ImagePath1);
});
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Identifying_second_file_filter).IsIndeterminate();
inputFilter2 = PluginRegister.Singleton.GetFilter(settings.ImagePath2);
});
if(inputFilter1 == null)
{
AaruLogging.Error(UI.Cannot_open_first_input_file);
return (int)ErrorNumber.CannotOpenFile;
}
if(inputFilter2 == null)
{
AaruLogging.Error(UI.Cannot_open_second_input_file);
return (int)ErrorNumber.CannotOpenFile;
}
IBaseImage input1Format = null;
IBaseImage input2Format = null;
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Identifying_first_image_format).IsIndeterminate();
input1Format = ImageFormat.Detect(inputFilter1);
});
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Identifying_second_image_format).IsIndeterminate();
input2Format = ImageFormat.Detect(inputFilter2);
});
if(input1Format == null)
{
AaruLogging.Error(UI.First_input_file_format_not_identified);
return (int)ErrorNumber.UnrecognizedFormat;
}
if(settings.Verbose)
AaruLogging.Verbose(UI.First_input_file_format_identified_by_0_1, input1Format.Name, input1Format.Id);
else
AaruLogging.WriteLine(UI.First_input_file_format_identified_by_0, input1Format.Name);
if(input2Format == null)
{
AaruLogging.Error(UI.Second_input_file_format_not_identified);
return (int)ErrorNumber.UnrecognizedFormat;
}
if(settings.Verbose)
AaruLogging.Verbose(UI.Second_input_file_format_identified_by_0_1, input2Format.Name, input2Format.Id);
else
AaruLogging.WriteLine(UI.Second_input_file_format_identified_by_0, input2Format.Name);
ErrorNumber opened1 = ErrorNumber.NoData;
ErrorNumber opened2 = ErrorNumber.NoData;
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Opening_first_image_file).IsIndeterminate();
opened1 = input1Format.Open(inputFilter1);
});
if(opened1 != ErrorNumber.NoError)
{
AaruLogging.WriteLine(UI.Unable_to_open_first_image_format);
AaruLogging.WriteLine(Localization.Core.Error_0, opened1);
return (int)opened1;
}
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Opening_second_image_file).IsIndeterminate();
opened2 = input2Format.Open(inputFilter2);
});
if(opened2 != ErrorNumber.NoError)
{
AaruLogging.WriteLine(UI.Unable_to_open_second_image_format);
AaruLogging.WriteLine(Localization.Core.Error_0, opened2);
return (int)opened2;
}
Statistics.AddMediaFormat(input1Format.Format);
Statistics.AddMediaFormat(input2Format.Format);
Statistics.AddMedia(input1Format.Info.MediaType, false);
Statistics.AddMedia(input2Format.Info.MediaType, false);
Statistics.AddFilter(inputFilter1.Name);
Statistics.AddFilter(inputFilter2.Name);
var sb = new StringBuilder();
Table table = new();
table.AddColumn("");
table.AddColumn(new TableColumn(new Markup(UI.Title_First_Media_image).Centered()));
table.AddColumn(new TableColumn(new Markup(UI.Title_Second_Media_image).Centered()));
table.Columns[0].RightAligned();
table.Border(TableBorder.Rounded);
table.BorderColor(Color.Yellow);
if(settings.Verbose)
{
table.AddRow(UI.Title_File,
$"[navy]{Markup.Escape(settings.ImagePath1)}[/]",
$"[navy]{Markup.Escape(settings.ImagePath2)}[/]");
table.AddRow(UI.Title_Media_image_format,
$"[fuchsia]{input1Format.Name}[/]",
$"[fuchsia]{input2Format.Name}[/]");
}
else
{
sb
.AppendFormat($"[bold][green]{UI.Title_First_Media_image}:[/] [italic]{Markup.Escape(settings.ImagePath1)}[/][/]")
.AppendLine();
sb
.AppendFormat($"[bold][red]{UI.Title_Second_Media_image}:[/] [italic]{Markup.Escape(settings.ImagePath2)}[/][/]")
.AppendLine();
}
bool imagesDiffer = false;
ErrorNumber errno;
ImageInfo image1Info = input1Format.Info;
ImageInfo image2Info = input2Format.Info;
Dictionary<MediaTagType, byte[]> image1DiskTags = [];
Dictionary<MediaTagType, byte[]> image2DiskTags = [];
var input1MediaImage = input1Format as IMediaImage;
var input2MediaImage = input2Format as IMediaImage;
if(input1MediaImage != null)
{
foreach(MediaTagType diskTag in Enum.GetValues(typeof(MediaTagType)))
{
errno = input1MediaImage.ReadMediaTag(diskTag, out byte[] tempArray);
if(errno == ErrorNumber.NoError) image1DiskTags.Add(diskTag, tempArray);
}
}
if(input2MediaImage != null)
{
foreach(MediaTagType diskTag in Enum.GetValues(typeof(MediaTagType)))
{
errno = input2MediaImage.ReadMediaTag(diskTag, out byte[] tempArray);
if(errno == ErrorNumber.NoError) image2DiskTags.Add(diskTag, tempArray);
}
}
if(settings.Verbose)
{
table.AddRow(UI.Has_partitions_Question,
image1Info.HasPartitions
? $"[green]{image1Info.HasPartitions}[/]"
: $"[red]{image1Info.HasPartitions}[/]",
image2Info.HasPartitions
? $"[green]{image2Info.HasPartitions}[/]"
: $"[red]{image2Info.HasPartitions}[/]");
table.AddRow(UI.Has_sessions_Question,
image1Info.HasSessions
? $"[green]{image1Info.HasSessions}[/]"
: $"[red]{image1Info.HasSessions}[/]",
image2Info.HasSessions
? $"[green]{image2Info.HasSessions}[/]"
: $"[red]{image2Info.HasSessions}[/]");
table.AddRow(UI.Title_Image_size, $"[teal]{image1Info.ImageSize}[/]", $"[teal]{image2Info.ImageSize}[/]");
table.AddRow(UI.Title_Sectors, $"[lime]{image1Info.Sectors}[/]", $"[lime]{image2Info.Sectors}[/]");
table.AddRow(UI.Title_Sector_size,
$"[aqua]{image1Info.SectorSize}[/]",
$"[aqua]{image2Info.SectorSize}[/]");
table.AddRow(UI.Title_Creation_time,
$"[yellow3]{image1Info.CreationTime.ToString(CultureInfo.CurrentCulture)}[/]",
$"[yellow3]{image2Info.CreationTime.ToString(CultureInfo.CurrentCulture)}[/]");
table.AddRow(UI.Title_Last_modification_time,
$"[yellow3]{image1Info.LastModificationTime.ToString(CultureInfo.CurrentCulture)}[/]",
$"[yellow3]{image2Info.LastModificationTime.ToString(CultureInfo.CurrentCulture)}[/]");
table.AddRow(UI.Title_Media_type,
$"[orange3]{image1Info.MediaType}[/]",
$"[orange3]{image2Info.MediaType}[/]");
table.AddRow(UI.Title_Image_version,
$"[rosybrown]{image1Info.Version ?? ""}[/]",
$"[rosybrown]{image2Info.Version ?? ""}[/]");
table.AddRow(UI.Title_Image_application,
$"[fuchsia]{image1Info.Application ?? ""}[/]",
$"[fuchsia]{image2Info.Application ?? ""}[/]");
table.AddRow(UI.Title_Image_application_version,
$"[rosybrown]{image1Info.ApplicationVersion ?? ""}[/]",
$"[rosybrown]{image2Info.ApplicationVersion ?? ""}[/]");
table.AddRow(UI.Title_Image_creator,
$"[blue]{Markup.Escape(image1Info.Creator ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.Creator ?? "")}[/]");
table.AddRow(UI.Title_Image_name,
$"[blue]{Markup.Escape(image1Info.MediaTitle ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaTitle ?? "")}[/]");
table.AddRow(UI.Title_Image_comments,
$"[blue]{Markup.Escape(image1Info.Comments ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.Comments ?? "")}[/]");
table.AddRow(UI.Title_Media_manufacturer,
$"[blue]{Markup.Escape(image1Info.MediaManufacturer ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaManufacturer ?? "")}[/]");
table.AddRow(UI.Title_Media_model,
$"[blue]{Markup.Escape(image1Info.MediaModel ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaModel ?? "")}[/]");
table.AddRow(UI.Title_Media_serial_number,
$"[blue]{Markup.Escape(image1Info.MediaSerialNumber ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaSerialNumber ?? "")}[/]");
table.AddRow(UI.Title_Media_barcode,
$"[blue]{Markup.Escape(image1Info.MediaBarcode ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaBarcode ?? "")}[/]");
table.AddRow(UI.Title_Media_part_number,
$"[blue]{Markup.Escape(image1Info.MediaPartNumber ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.MediaPartNumber ?? "")}[/]");
table.AddRow(UI.Title_Media_sequence,
$"[teal]{image1Info.MediaSequence}[/]",
$"[teal]{image2Info.MediaSequence}[/]");
table.AddRow(UI.Title_Last_media_on_sequence,
$"[teal]{image1Info.LastMediaSequence}[/]",
$"[teal]{image2Info.LastMediaSequence}[/]");
table.AddRow(UI.Title_Drive_manufacturer,
$"[blue]{Markup.Escape(image1Info.DriveManufacturer ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.DriveManufacturer ?? "")}[/]");
table.AddRow(UI.Title_Drive_firmware_revision,
$"[blue]{Markup.Escape(image1Info.DriveFirmwareRevision ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.DriveFirmwareRevision ?? "")}[/]");
table.AddRow(UI.Title_Drive_model,
$"[blue]{Markup.Escape(image1Info.DriveModel ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.DriveModel ?? "")}[/]");
table.AddRow(UI.Title_Drive_serial_number,
$"[blue]{Markup.Escape(image1Info.DriveSerialNumber ?? "")}[/]",
$"[blue]{Markup.Escape(image2Info.DriveSerialNumber ?? "")}[/]");
foreach(MediaTagType diskTag in
(Enum.GetValues(typeof(MediaTagType)) as MediaTagType[]).OrderBy(e => e.ToString()))
{
table.AddRow(string.Format(UI.Has_tag_0_Question, diskTag),
image1DiskTags.ContainsKey(diskTag)
? $"[green]{image1DiskTags.ContainsKey(diskTag)}[/]"
: $"[red]{image1DiskTags.ContainsKey(diskTag)}[/]",
image2DiskTags.ContainsKey(diskTag)
? $"[green]{image2DiskTags.ContainsKey(diskTag)}[/]"
: $"[red]{image2DiskTags.ContainsKey(diskTag)}[/]");
}
}
ulong leastSectors = 0;
Core.Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask(UI.Comparing_media_image_characteristics).IsIndeterminate();
if(image1Info.HasPartitions != image2Info.HasPartitions)
{
imagesDiffer = true;
if(!settings.Verbose) sb.AppendLine(UI.Image_partitioned_status_differ);
}
if(image1Info.HasSessions != image2Info.HasSessions)
{
imagesDiffer = true;
if(!settings.Verbose) sb.AppendLine(UI.Image_session_status_differ);
}
if(image1Info.Sectors != image2Info.Sectors)
{
imagesDiffer = true;
if(!settings.Verbose) sb.AppendLine(UI.Image_sectors_differ);
}
if(image1Info.SectorSize != image2Info.SectorSize)
{
imagesDiffer = true;
if(!settings.Verbose) sb.AppendLine(UI.Image_sector_size_differ);
}
if(image1Info.MediaType != image2Info.MediaType)
{
imagesDiffer = true;
if(!settings.Verbose) sb.AppendLine(UI.Media_type_differs);
}
if(image1Info.Sectors < image2Info.Sectors)
{
imagesDiffer = true;
leastSectors = image1Info.Sectors;
if(!settings.Verbose) sb.AppendLine(UI.Second_image_has_more_sectors);
}
else if(image1Info.Sectors > image2Info.Sectors)
{
imagesDiffer = true;
leastSectors = image2Info.Sectors;
if(!settings.Verbose) sb.AppendLine(UI.First_image_has_more_sectors);
}
else
leastSectors = image1Info.Sectors;
});
var input1ByteAddressable = input1Format as IByteAddressableImage;
var input2ByteAddressable = input2Format as IByteAddressableImage;
if(input1ByteAddressable is null && input2ByteAddressable is not null) imagesDiffer = true;
if(input1ByteAddressable is not null && input2ByteAddressable is null) imagesDiffer = true;
if(input1MediaImage is null && input2MediaImage is not null) imagesDiffer = true;
if(input1MediaImage is not null && input2MediaImage is null) imagesDiffer = true;
if(input1MediaImage is not null && input2MediaImage is not null)
{
AnsiConsole.Progress()
.AutoClear(true)
.HideCompleted(true)
.Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn())
.Start(ctx =>
{
ProgressTask task = ctx.AddTask(UI.Comparing_sectors);
task.MaxValue = leastSectors;
for(ulong sector = 0; sector < leastSectors; sector++)
{
task.Value = sector;
task.Description = string.Format(UI.Comparing_sector_0_of_1, sector + 1, leastSectors);
try
{
errno = input1MediaImage.ReadSector(sector, out byte[] image1Sector);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Error(string.Format(UI.Error_0_reading_sector_1_from_first_image,
errno,
sector));
}
errno = input2MediaImage.ReadSector(sector, out byte[] image2Sector);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Error(string.Format(UI.Error_0_reading_sector_1_from_second_image,
errno,
sector));
}
ArrayHelpers.CompareBytes(out bool different,
out bool sameSize,
image1Sector,
image2Sector);
if(different)
imagesDiffer = true;
// sb.AppendFormat("Sector {0} is different", sector).AppendLine();
else if(!sameSize) imagesDiffer = true;
/* sb.
AppendFormat("Sector {0} has different sizes ({1} bytes in image 1, {2} in image 2) but are otherwise identical",
sector, image1Sector.LongLength, image2Sector.LongLength).AppendLine();*/
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
});
}
if(input1ByteAddressable is not null && input2ByteAddressable is not null)
{
AnsiConsole.Progress()
.AutoClear(true)
.HideCompleted(true)
.Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn())
.Start(ctx =>
{
ProgressTask task = ctx.AddTask(UI.Comparing_images);
task.IsIndeterminate = true;
byte[] data1 = new byte[input1ByteAddressable.Info.Sectors];
byte[] data2 = new byte[input2ByteAddressable.Info.Sectors];
byte[] tmp;
input1ByteAddressable.ReadBytes(data1, 0, data1.Length, out int bytesRead);
if(bytesRead != data1.Length)
{
tmp = new byte[bytesRead];
Array.Copy(data1, 0, tmp, 0, bytesRead);
data1 = tmp;
}
input2ByteAddressable.ReadBytes(data2, 0, data2.Length, out bytesRead);
if(bytesRead != data2.Length)
{
tmp = new byte[bytesRead];
Array.Copy(data2, 0, tmp, 0, bytesRead);
data2 = tmp;
}
ArrayHelpers.CompareBytes(out bool different, out bool sameSize, data1, data2);
if(different)
imagesDiffer = true;
else if(!sameSize) imagesDiffer = true;
});
}
AaruLogging.WriteLine();
sb.AppendLine(imagesDiffer ? UI.Images_differ : UI.Images_do_not_differ);
if(settings.Verbose)
AnsiConsole.Write(table);
else
AaruLogging.WriteLine(sb.ToString());
return (int)ErrorNumber.NoError;
}
#region Nested type: Settings
public class Settings : ImageFamily
{
[Description("First media image path")]
[CommandArgument(0, "<image-path1>")]
public string ImagePath1 { get; init; }
[Description("Second media image path")]
[CommandArgument(1, "<image-path1>")]
public string ImagePath2 { get; init; }
}
#endregion
}