Files
Aaru/Aaru/Commands/Image/Convert.cs

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