mirror of
https://github.com/aaru-dps/Aaru.git
synced 2025-12-16 19:24:25 +00:00
743 lines
31 KiB
C#
743 lines
31 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Convert.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Commands.
|
|
//
|
|
// --[ Description ] ----------------------------------------------------------
|
|
//
|
|
// Converts from one media image to another.
|
|
//
|
|
// --[ 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.IO;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Xml.Serialization;
|
|
using Aaru.CommonTypes;
|
|
using Aaru.CommonTypes.AaruMetadata;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Metadata;
|
|
using Aaru.Core;
|
|
using Aaru.Localization;
|
|
using Aaru.Logging;
|
|
using Schemas;
|
|
using Spectre.Console;
|
|
using Spectre.Console.Cli;
|
|
using Convert = Aaru.Core.Image.Convert;
|
|
using File = System.IO.File;
|
|
using MediaType = Aaru.CommonTypes.MediaType;
|
|
|
|
namespace Aaru.Commands.Image;
|
|
|
|
sealed class ConvertImageCommand : Command<ConvertImageCommand.Settings>
|
|
{
|
|
const string MODULE_NAME = "Convert-image command";
|
|
static ProgressTask _progressTask1;
|
|
static ProgressTask _progressTask2;
|
|
|
|
public override int Execute(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("convert-image");
|
|
|
|
// Log all command parameters for debugging and auditing
|
|
Dictionary<string, string> parsedOptions = Options.Parse(settings.Options);
|
|
LogCommandParameters(settings, fixSubchannelPosition, fixSubchannel, fixSubchannelCrc, parsedOptions);
|
|
|
|
// Validate sector count parameter
|
|
if(settings.Count == 0)
|
|
{
|
|
AaruLogging.Error(UI.Need_to_specify_more_than_zero_sectors_to_copy_at_once);
|
|
|
|
return (int)ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Parse and validate CHS geometry if specified
|
|
(bool success, uint cylinders, uint heads, uint sectors)? geometryResult = ParseGeometry(settings.Geometry);
|
|
(uint cylinders, uint heads, uint sectors)? geometryValues = null;
|
|
|
|
if(geometryResult.HasValue)
|
|
{
|
|
if(!geometryResult.Value.success) return (int)ErrorNumber.InvalidArgument;
|
|
|
|
geometryValues = (geometryResult.Value.cylinders, geometryResult.Value.heads, geometryResult.Value.sectors);
|
|
}
|
|
|
|
// Load metadata and resume information from sidecar files
|
|
Resume resume = null;
|
|
Metadata sidecar = null;
|
|
MediaType mediaType;
|
|
|
|
(bool success, Metadata sidecar, Resume resume) metadataResult =
|
|
LoadMetadata(settings.AaruMetadata, settings.CicmXml, settings.ResumeFile);
|
|
|
|
if(!metadataResult.success) return (int)ErrorNumber.InvalidArgument;
|
|
|
|
sidecar = metadataResult.sidecar;
|
|
resume = metadataResult.resume;
|
|
|
|
// Identify input file filter (determines file type handler)
|
|
IFilter inputFilter = null;
|
|
|
|
Core.Spectre.ProgressSingleSpinner(ctx =>
|
|
{
|
|
ctx.AddTask(UI.Identifying_file_filter).IsIndeterminate();
|
|
inputFilter = PluginRegister.Singleton.GetFilter(settings.InputPath);
|
|
});
|
|
|
|
if(inputFilter == null)
|
|
{
|
|
AaruLogging.Error(UI.Cannot_open_specified_file);
|
|
|
|
return (int)ErrorNumber.CannotOpenFile;
|
|
}
|
|
|
|
// Verify output file doesn't already exist
|
|
if(File.Exists(settings.OutputPath))
|
|
{
|
|
AaruLogging.Error(UI.Output_file_already_exists);
|
|
|
|
return (int)ErrorNumber.FileExists;
|
|
}
|
|
|
|
// Identify input image format
|
|
PluginRegister plugins = PluginRegister.Singleton;
|
|
IMediaImage inputFormat = null;
|
|
IBaseImage baseImage = null;
|
|
|
|
Core.Spectre.ProgressSingleSpinner(ctx =>
|
|
{
|
|
ctx.AddTask(UI.Identifying_image_format).IsIndeterminate();
|
|
baseImage = ImageFormat.Detect(inputFilter);
|
|
inputFormat = baseImage as IMediaImage;
|
|
});
|
|
|
|
if(inputFormat == null)
|
|
{
|
|
AaruLogging.WriteLine(UI.Input_image_format_not_identified);
|
|
|
|
return (int)ErrorNumber.UnrecognizedFormat;
|
|
}
|
|
|
|
// TODO: Implement
|
|
if(inputFormat == null)
|
|
{
|
|
AaruLogging.WriteLine(UI.Command_not_yet_supported_for_this_image_type);
|
|
|
|
return (int)ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
if(settings.Verbose)
|
|
AaruLogging.Verbose(UI.Input_image_format_identified_by_0_1, inputFormat.Name, inputFormat.Id);
|
|
else
|
|
AaruLogging.WriteLine(UI.Input_image_format_identified_by_0, inputFormat.Name);
|
|
|
|
uint nominalNegativeSectors = 0;
|
|
uint nominalOverflowSectors = 0;
|
|
|
|
try
|
|
{
|
|
// Open the input image file for reading
|
|
ErrorNumber opened = ErrorNumber.NoData;
|
|
|
|
Core.Spectre.ProgressSingleSpinner(ctx =>
|
|
{
|
|
ctx.AddTask(UI.Invoke_Opening_image_file).IsIndeterminate();
|
|
opened = inputFormat.Open(inputFilter);
|
|
});
|
|
|
|
if(opened != ErrorNumber.NoError)
|
|
{
|
|
AaruLogging.WriteLine(UI.Unable_to_open_image_format);
|
|
AaruLogging.WriteLine(Localization.Core.Error_0, opened);
|
|
|
|
return (int)opened;
|
|
}
|
|
|
|
nominalNegativeSectors = settings.IgnoreNegativeSectors ? 0 : inputFormat.Info.NegativeSectors;
|
|
nominalOverflowSectors = settings.IgnoreOverflowSectors ? 0 : inputFormat.Info.OverflowSectors;
|
|
|
|
// Get media type and handle obsolete type mappings for backwards compatibility
|
|
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);
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
AaruLogging.Error(UI.Unable_to_open_image_format);
|
|
AaruLogging.Error(Localization.Core.Error_0, ex.Message);
|
|
AaruLogging.Exception(ex, Localization.Core.Error_0, ex.Message);
|
|
|
|
return (int)ErrorNumber.CannotOpenFormat;
|
|
}
|
|
|
|
// Discover and load output format plugin
|
|
IBaseWritableImage outputFormat = FindOutputFormat(plugins, settings.Format, settings.OutputPath);
|
|
|
|
if(outputFormat == null) return (int)ErrorNumber.FormatNotFound;
|
|
|
|
if(settings.Verbose)
|
|
AaruLogging.Verbose(UI.Output_image_format_0_1, outputFormat.Name, outputFormat.Id);
|
|
else
|
|
AaruLogging.WriteLine(UI.Output_image_format_0, outputFormat.Name);
|
|
|
|
var converter = new Convert(inputFormat,
|
|
outputFormat as IWritableImage,
|
|
mediaType,
|
|
settings.Force,
|
|
settings.OutputPath,
|
|
parsedOptions,
|
|
nominalNegativeSectors,
|
|
nominalOverflowSectors,
|
|
settings.Comments,
|
|
settings.Creator,
|
|
settings.DriveFirmwareRevision,
|
|
settings.DriveManufacturer,
|
|
settings.DriveModel,
|
|
settings.DriveSerialNumber,
|
|
settings.LastMediaSequence,
|
|
settings.MediaBarcode,
|
|
settings.MediaManufacturer,
|
|
settings.MediaModel,
|
|
settings.MediaPartNumber,
|
|
settings.MediaSequence,
|
|
settings.MediaSerialNumber,
|
|
settings.MediaTitle,
|
|
settings.Decrypt,
|
|
(uint)settings.Count,
|
|
plugins,
|
|
fixSubchannelPosition,
|
|
fixSubchannel,
|
|
fixSubchannelCrc,
|
|
settings.GenerateSubchannels,
|
|
geometryValues,
|
|
resume,
|
|
sidecar);
|
|
|
|
ErrorNumber errno = ErrorNumber.NoError;
|
|
|
|
AnsiConsole.Progress()
|
|
.AutoClear(true)
|
|
.HideCompleted(true)
|
|
.Columns(new ProgressBarColumn(), new PercentageColumn(), new TaskDescriptionColumn())
|
|
.Start(ctx =>
|
|
{
|
|
converter.UpdateStatus += static text => AaruLogging.WriteLine(text);
|
|
|
|
converter.ErrorMessage += static text => AaruLogging.Error(text);
|
|
|
|
converter.StoppingErrorMessage += static text => AaruLogging.Error(text);
|
|
|
|
converter.UpdateProgress += (text, current, maximum) =>
|
|
{
|
|
_progressTask1 ??= ctx.AddTask("Progress");
|
|
_progressTask1.Description = text;
|
|
_progressTask1.Value = current;
|
|
_progressTask1.MaxValue = maximum;
|
|
};
|
|
|
|
converter.PulseProgress += text =>
|
|
{
|
|
if(_progressTask1 is null)
|
|
ctx.AddTask(text).IsIndeterminate();
|
|
else
|
|
{
|
|
_progressTask1.Description = text;
|
|
_progressTask1.IsIndeterminate = true;
|
|
}
|
|
};
|
|
|
|
converter.InitProgress += () => _progressTask1 = ctx.AddTask("Progress");
|
|
|
|
converter.EndProgress += static () =>
|
|
{
|
|
_progressTask1?.StopTask();
|
|
_progressTask1 = null;
|
|
};
|
|
|
|
converter.InitProgress2 += () => _progressTask2 = ctx.AddTask("Progress");
|
|
|
|
converter.EndProgress2 += static () =>
|
|
{
|
|
_progressTask2?.StopTask();
|
|
_progressTask2 = null;
|
|
};
|
|
|
|
converter.UpdateProgress2 += (text, current, maximum) =>
|
|
{
|
|
_progressTask2 ??= ctx.AddTask("Progress");
|
|
_progressTask2.Description = text;
|
|
_progressTask2.Value = current;
|
|
_progressTask2.MaxValue = maximum;
|
|
};
|
|
|
|
Console.CancelKeyPress += (_, e) =>
|
|
{
|
|
e.Cancel = true;
|
|
converter.Abort();
|
|
};
|
|
|
|
errno = converter.Start();
|
|
});
|
|
|
|
return (int)errno;
|
|
}
|
|
|
|
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)
|
|
{
|
|
AaruLogging.Error(UI.Invalid_geometry_specified);
|
|
|
|
return (false, 0, 0, 0);
|
|
}
|
|
|
|
if(!uint.TryParse(geometryPieces[0], out uint cylinders) || cylinders == 0)
|
|
{
|
|
AaruLogging.Error(UI.Invalid_number_of_cylinders_specified);
|
|
|
|
return (false, 0, 0, 0);
|
|
}
|
|
|
|
if(!uint.TryParse(geometryPieces[1], out uint heads) || heads == 0)
|
|
{
|
|
AaruLogging.Error(UI.Invalid_number_of_heads_specified);
|
|
|
|
return (false, 0, 0, 0);
|
|
}
|
|
|
|
if(uint.TryParse(geometryPieces[2], out uint sectors) && sectors != 0) return (true, cylinders, heads, sectors);
|
|
|
|
AaruLogging.Error(UI.Invalid_sectors_per_track_specified);
|
|
|
|
return (false, 0, 0, 0);
|
|
}
|
|
|
|
private (bool success, Metadata sidecar, Resume resume) LoadMetadata(
|
|
string aaruMetadataPath, string cicmXmlPath, string resumeFilePath)
|
|
{
|
|
// Loads metadata and resume information from sidecar files
|
|
// Supports both Aaru JSON and legacy CICM XML formats
|
|
// Returns tuple with success flag, metadata, and resume information
|
|
|
|
Metadata sidecar = null;
|
|
Resume resume = null;
|
|
|
|
if(aaruMetadataPath != null)
|
|
{
|
|
if(File.Exists(aaruMetadataPath))
|
|
{
|
|
try
|
|
{
|
|
var fs = new FileStream(aaruMetadataPath, FileMode.Open);
|
|
|
|
sidecar =
|
|
(JsonSerializer.Deserialize(fs, typeof(MetadataJson), MetadataJsonContext.Default) as
|
|
MetadataJson)?.AaruMetadata;
|
|
|
|
fs.Close();
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
AaruLogging.Error(UI.Incorrect_metadata_sidecar_file_not_continuing);
|
|
AaruLogging.Exception(ex, UI.Incorrect_metadata_sidecar_file_not_continuing);
|
|
|
|
return (false, null, null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AaruLogging.Error(UI.Could_not_find_metadata_sidecar);
|
|
|
|
return (false, null, null);
|
|
}
|
|
}
|
|
else if(cicmXmlPath != null)
|
|
{
|
|
if(File.Exists(cicmXmlPath))
|
|
{
|
|
try
|
|
{
|
|
// Should be covered by virtue of being the same exact class as the JSON above
|
|
#pragma warning disable IL2026, CS0618
|
|
var xs = new XmlSerializer(typeof(CICMMetadataType));
|
|
#pragma warning restore IL2026, CS0618
|
|
|
|
var sr = new StreamReader(cicmXmlPath);
|
|
|
|
// Should be covered by virtue of being the same exact class as the JSON above
|
|
#pragma warning disable IL2026, CS0618
|
|
sidecar = (CICMMetadataType)xs.Deserialize(sr);
|
|
#pragma warning restore IL2026, CS0618
|
|
|
|
sr.Close();
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
AaruLogging.Error(UI.Incorrect_metadata_sidecar_file_not_continuing);
|
|
AaruLogging.Exception(ex, UI.Incorrect_metadata_sidecar_file_not_continuing);
|
|
|
|
return (false, null, null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AaruLogging.Error(UI.Could_not_find_metadata_sidecar);
|
|
|
|
return (false, null, null);
|
|
}
|
|
}
|
|
|
|
if(resumeFilePath == null) return (true, sidecar, null);
|
|
|
|
if(File.Exists(resumeFilePath))
|
|
{
|
|
try
|
|
{
|
|
if(resumeFilePath.EndsWith(".metadata.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)
|
|
{
|
|
AaruLogging.Error(UI.Incorrect_resume_file_not_continuing);
|
|
AaruLogging.Exception(ex, UI.Incorrect_resume_file_not_continuing);
|
|
|
|
return (false, sidecar, null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AaruLogging.Error(UI.Could_not_find_resume_file);
|
|
|
|
return (false, sidecar, null);
|
|
}
|
|
|
|
return (true, sidecar, resume);
|
|
}
|
|
|
|
private void LogCommandParameters(Settings settings, bool fixSubchannelPosition, bool fixSubchannel,
|
|
bool fixSubchannelCrc, Dictionary<string, string> parsedOptions)
|
|
{
|
|
// Logs all command-line parameters for debugging and audit trail purposes
|
|
// Consolidated from 46+ individual logging statements
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "--cicm-xml={0}", Markup.Escape(settings.CicmXml ?? ""));
|
|
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, "--force={0}", settings.Force);
|
|
AaruLogging.Debug(MODULE_NAME, "--format={0}", Markup.Escape(settings.Format ?? ""));
|
|
AaruLogging.Debug(MODULE_NAME, "--geometry={0}", Markup.Escape(settings.Geometry ?? ""));
|
|
AaruLogging.Debug(MODULE_NAME, "--input={0}", Markup.Escape(settings.InputPath ?? ""));
|
|
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, "--output={0}", Markup.Escape(settings.OutputPath ?? ""));
|
|
AaruLogging.Debug(MODULE_NAME, "--resume-file={0}", Markup.Escape(settings.ResumeFile ?? ""));
|
|
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, "--aaru-metadata={0}", Markup.Escape(settings.AaruMetadata ?? ""));
|
|
AaruLogging.Debug(MODULE_NAME, "--ignore-negative-sectors={0}", settings.IgnoreNegativeSectors);
|
|
AaruLogging.Debug(MODULE_NAME, "--ignore-overflow-sectors={0}", settings.IgnoreOverflowSectors);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, UI.Parsed_options);
|
|
|
|
foreach(KeyValuePair<string, string> parsedOption in parsedOptions)
|
|
AaruLogging.Debug(MODULE_NAME, "{0} = {1}", parsedOption.Key, parsedOption.Value);
|
|
}
|
|
|
|
|
|
private IBaseWritableImage FindOutputFormat(PluginRegister plugins, string format, string outputPath)
|
|
{
|
|
// Discovers output format plugin by extension, GUID, or name
|
|
// Searches writable format plugins matching any of three methods:
|
|
// 1. By file extension (if format not specified)
|
|
// 2. By plugin GUID (if format is valid GUID)
|
|
// 3. By plugin name (case-insensitive string match)
|
|
// Returns null if no match or multiple matches found
|
|
|
|
List<IBaseWritableImage> candidates = [];
|
|
|
|
// Try extension
|
|
if(string.IsNullOrEmpty(format))
|
|
{
|
|
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
|
where plugin is not null
|
|
where plugin.KnownExtensions.Contains(Path.GetExtension(outputPath))
|
|
select plugin);
|
|
}
|
|
|
|
// Try Id
|
|
else if(Guid.TryParse(format, out Guid outId))
|
|
{
|
|
candidates.AddRange(from plugin in plugins.WritableImages.Values
|
|
where plugin is not null
|
|
where plugin.Id.Equals(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:
|
|
AaruLogging.WriteLine(UI.No_plugin_supports_requested_extension);
|
|
|
|
return null;
|
|
case > 1:
|
|
AaruLogging.WriteLine(UI.More_than_one_plugin_supports_requested_extension);
|
|
|
|
return null;
|
|
}
|
|
|
|
return candidates[0];
|
|
}
|
|
|
|
|
|
public class Settings : ImageFamily
|
|
{
|
|
[LocalizedDescription(nameof(UI.Take_metadata_from_existing_CICM_XML_sidecar))]
|
|
[DefaultValue(null)]
|
|
[CommandOption("-x|--cicm-xml")]
|
|
public string CicmXml { 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.Continue_conversion_even_if_data_lost))]
|
|
[DefaultValue(false)]
|
|
[CommandOption("-f|--force")]
|
|
public bool Force { 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.Take_dump_hardware_from_existing_resume))]
|
|
[DefaultValue(null)]
|
|
[CommandOption("-r|--resume-file")]
|
|
public string ResumeFile { 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.Take_metadata_from_existing_Aaru_sidecar))]
|
|
[DefaultValue(null)]
|
|
[CommandOption("-m|--aaru-metadata")]
|
|
public string AaruMetadata { get; init; }
|
|
[LocalizedDescription(nameof(UI.Input_image_path))]
|
|
[CommandArgument(0, "<input-image>")]
|
|
public string InputPath { get; init; }
|
|
[LocalizedDescription(nameof(UI.Output_image_path))]
|
|
[CommandArgument(1, "<output-image>")]
|
|
public string OutputPath { 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; }
|
|
}
|
|
} |