Files
Aaru/Aaru/Commands/Media/Dump.cs

707 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Dump.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Commands.
//
// --[ Description ] ----------------------------------------------------------
//
// Implements the 'dump' 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-2022 Natalia Portillo
// Copyright © 2020-2022 Rebecca Wallander
// ****************************************************************************/
namespace Aaru.Commands.Media;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Xml.Serialization;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Interop;
using Aaru.CommonTypes.Metadata;
using Aaru.CommonTypes.Structs.Devices.SCSI;
using Aaru.Console;
using Aaru.Core;
using Aaru.Core.Devices.Dumping;
using Aaru.Core.Logging;
using Aaru.Devices;
using Schemas;
using Spectre.Console;
// TODO: Add raw dumping
sealed class DumpMediaCommand : Command
{
static ProgressTask _progressTask1;
static ProgressTask _progressTask2;
public DumpMediaCommand() : base("dump", "Dumps the media inserted on a device to a media image.")
{
Add(new Option<string>(new[]
{
"--cicm-xml", "-x"
}, () => null, "Take metadata from existing CICM XML sidecar."));
Add(new Option<string>(new[]
{
"--encoding", "-e"
}, () => null, "Name of character encoding to use."));
Add(new Option<bool>("--first-pregap", () => false,
"Try to read first track pregap. Only applicable to CD/DDCD/GD."));
Add(new Option<bool>("--fix-offset", () => true, "Fix audio tracks offset. Only applicable to CD/GD."));
Add(new Option<bool>(new[]
{
"--force", "-f"
}, () => false, "Continue dump whatever happens."));
Add(new Option<string>(new[]
{
"--format", "-t"
}, () => null,
"Format of the output image, as plugin name or plugin id. If not present, will try to detect it from output image extension."));
Add(new Option<bool>("--metadata", () => true, "Enables creating CICM XML sidecar."));
Add(new Option<bool>("--trim", () => true, "Enables trimming errored from skipped sectors."));
Add(new Option<string>(new[]
{
"--options", "-O"
}, () => null, "Comma separated name=value pairs of options to pass to output image plugin."));
Add(new Option<bool>("--persistent", () => false, "Try to recover partial or incorrect data."));
Add(new Option<bool>(new[]
{
"--resume", "-r"
}, () => true, "Create/use resume mapfile."));
Add(new Option<ushort>(new[]
{
"--retry-passes", "-p"
}, () => 5, "How many retry passes to do."));
Add(new Option<uint>(new[]
{
"--skip", "-k"
}, () => 512, "When an unreadable sector is found skip this many sectors."));
Add(new Option<bool>(new[]
{
"--stop-on-error", "-s"
}, () => false, "Stop media dump on first error."));
Add(new Option<string>("--subchannel", () => "any",
"Subchannel to dump. Only applicable to CD/GD. Values: any, rw, rw-or-pq, pq, none."));
Add(new Option<byte>("--speed", () => 0, "Speed to dump. Only applicable to optical drives, 0 for maximum."));
AddArgument(new Argument<string>
{
Arity = ArgumentArity.ExactlyOne,
Description = "Device path",
Name = "device-path"
});
AddArgument(new Argument<string>
{
Arity = ArgumentArity.ExactlyOne,
Description =
"Output image path. If filename starts with # and exists, it will be read as a list of output images, its extension will be used to detect the image output format, each media will be ejected and confirmation for the next one will be asked.",
Name = "output-path"
});
Add(new Option<bool>(new[]
{
"--private"
}, () => false, "Do not store paths and serial numbers in log or metadata."));
Add(new Option<bool>(new[]
{
"--fix-subchannel-position"
}, () => true, "Store subchannel according to the sector they describe."));
Add(new Option<bool>(new[]
{
"--retry-subchannel"
}, () => true, "Retry subchannel. Implies fixing subchannel position."));
Add(new Option<bool>(new[]
{
"--fix-subchannel"
}, () => false, "Try to fix subchannel. Implies fixing subchannel position."));
Add(new Option<bool>(new[]
{
"--fix-subchannel-crc"
}, () => false, "If subchannel looks OK but CRC fails, rewrite it. Implies fixing subchannel."));
Add(new Option<bool>(new[]
{
"--generate-subchannels"
}, () => false, "Generates missing subchannels (they don't count as dumped in resume file)."));
Add(new Option<bool>(new[]
{
"--skip-cdiready-hole"
}, () => true, "Skip the hole between data and audio in a CD-i Ready disc."));
Add(new Option<bool>(new[]
{
"--eject"
}, () => false, "Eject media after dump finishes."));
Add(new Option<uint>(new[]
{
"--max-blocks"
}, () => 64, "Maximum number of blocks to read at once."));
Add(new Option<bool>(new[]
{
"--use-buffered-reads"
}, () => true, "For MMC/SD, use OS buffered reads if CMD23 is not supported."));
Add(new Option<bool>(new[]
{
"--store-encrypted"
}, () => true, "Store encrypted data as is."));
Add(new Option<bool>(new[]
{
"--title-keys"
}, () => true, "Try to read the title keys from CSS encrypted DVDs (very slow)."));
Add(new Option<uint>(new[]
{
"--ignore-cdr-runouts"
}, () => 10, "How many CD-R(W) run-out sectors to ignore and regenerate (0 for none)."));
Handler = CommandHandler.Create(GetType().GetMethod(nameof(Invoke)));
}
public static int Invoke(bool debug, bool verbose, string cicmXml, string devicePath, bool resume, string encoding,
bool firstPregap, bool fixOffset, bool force, bool metadata, bool trim, string outputPath,
string options, bool persistent, ushort retryPasses, uint skip, byte speed,
bool stopOnError, string format, string subchannel, bool @private,
bool fixSubchannelPosition, bool retrySubchannel, bool fixSubchannel,
bool fixSubchannelCrc, bool generateSubchannels, bool skipCdiReadyHole, bool eject,
uint maxBlocks, bool useBufferedReads, bool storeEncrypted, bool titleKeys,
uint ignoreCdrRunOuts)
{
MainClass.PrintCopyright();
if(debug)
{
IAnsiConsole stderrConsole = AnsiConsole.Create(new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput(Console.Error)
});
AaruConsole.DebugWriteLineEvent += (format, objects) =>
{
if(objects is null)
stderrConsole.MarkupLine(format);
else
stderrConsole.MarkupLine(format, objects);
};
}
if(verbose)
AaruConsole.WriteEvent += (format, objects) =>
{
if(objects is null)
AnsiConsole.Markup(format);
else
AnsiConsole.Markup(format, objects);
};
fixSubchannel |= fixSubchannelCrc;
fixSubchannelPosition |= retrySubchannel || fixSubchannel;
if(maxBlocks == 0)
maxBlocks = 64;
Statistics.AddCommand("dump-media");
AaruConsole.DebugWriteLine("Dump-Media command", "--cicm-xml={0}", cicmXml);
AaruConsole.DebugWriteLine("Dump-Media command", "--debug={0}", debug);
AaruConsole.DebugWriteLine("Dump-Media command", "--device={0}", devicePath);
AaruConsole.DebugWriteLine("Dump-Media command", "--encoding={0}", encoding);
AaruConsole.DebugWriteLine("Dump-Media command", "--first-pregap={0}", firstPregap);
AaruConsole.DebugWriteLine("Dump-Media command", "--fix-offset={0}", fixOffset);
AaruConsole.DebugWriteLine("Dump-Media command", "--force={0}", force);
AaruConsole.DebugWriteLine("Dump-Media command", "--format={0}", format);
AaruConsole.DebugWriteLine("Dump-Media command", "--metadata={0}", metadata);
AaruConsole.DebugWriteLine("Dump-Media command", "--options={0}", options);
AaruConsole.DebugWriteLine("Dump-Media command", "--output={0}", outputPath);
AaruConsole.DebugWriteLine("Dump-Media command", "--persistent={0}", persistent);
AaruConsole.DebugWriteLine("Dump-Media command", "--resume={0}", resume);
AaruConsole.DebugWriteLine("Dump-Media command", "--retry-passes={0}", retryPasses);
AaruConsole.DebugWriteLine("Dump-Media command", "--skip={0}", skip);
AaruConsole.DebugWriteLine("Dump-Media command", "--stop-on-error={0}", stopOnError);
AaruConsole.DebugWriteLine("Dump-Media command", "--trim={0}", trim);
AaruConsole.DebugWriteLine("Dump-Media command", "--verbose={0}", verbose);
AaruConsole.DebugWriteLine("Dump-Media command", "--subchannel={0}", subchannel);
AaruConsole.DebugWriteLine("Dump-Media command", "--private={0}", @private);
AaruConsole.DebugWriteLine("Dump-Media command", "--fix-subchannel-position={0}", fixSubchannelPosition);
AaruConsole.DebugWriteLine("Dump-Media command", "--retry-subchannel={0}", retrySubchannel);
AaruConsole.DebugWriteLine("Dump-Media command", "--fix-subchannel={0}", fixSubchannel);
AaruConsole.DebugWriteLine("Dump-Media command", "--fix-subchannel-crc={0}", fixSubchannelCrc);
AaruConsole.DebugWriteLine("Dump-Media command", "--generate-subchannels={0}", generateSubchannels);
AaruConsole.DebugWriteLine("Dump-Media command", "--skip-cdiready-hole={0}", skipCdiReadyHole);
AaruConsole.DebugWriteLine("Dump-Media command", "--eject={0}", eject);
AaruConsole.DebugWriteLine("Dump-Media command", "--max-blocks={0}", maxBlocks);
AaruConsole.DebugWriteLine("Dump-Media command", "--use-buffered-reads={0}", useBufferedReads);
AaruConsole.DebugWriteLine("Dump-Media command", "--store-encrypted={0}", storeEncrypted);
AaruConsole.DebugWriteLine("Dump-Media command", "--title-keys={0}", titleKeys);
AaruConsole.DebugWriteLine("Dump-Media command", "--ignore-cdr-runouts={0}", ignoreCdrRunOuts);
// TODO: Disabled temporarily
//AaruConsole.DebugWriteLine("Dump-Media command", "--raw={0}", raw);
Dictionary<string, string> parsedOptions = Core.Options.Parse(options);
AaruConsole.DebugWriteLine("Dump-Media command", "Parsed options:");
foreach(KeyValuePair<string, string> parsedOption in parsedOptions)
AaruConsole.DebugWriteLine("Dump-Media command", "{0} = {1}", parsedOption.Key, parsedOption.Value);
Encoding encodingClass = null;
if(encoding != null)
try
{
encodingClass = Claunia.Encoding.Encoding.GetEncoding(encoding);
if(verbose)
AaruConsole.VerboseWriteLine("Using encoding for {0}.", encodingClass.EncodingName);
}
catch(ArgumentException)
{
AaruConsole.ErrorWriteLine("Specified encoding is not supported.");
return (int)ErrorNumber.EncodingUnknown;
}
DumpSubchannel wantedSubchannel = DumpSubchannel.Any;
switch(subchannel?.ToLowerInvariant())
{
case "any":
case null:
wantedSubchannel = DumpSubchannel.Any;
break;
case "rw":
wantedSubchannel = DumpSubchannel.Rw;
break;
case "rw-or-pq":
wantedSubchannel = DumpSubchannel.RwOrPq;
break;
case "pq":
wantedSubchannel = DumpSubchannel.Pq;
break;
case "none":
wantedSubchannel = DumpSubchannel.None;
break;
default:
AaruConsole.WriteLine("Incorrect subchannel type \"{0}\" requested.", subchannel);
break;
}
string filename = Path.GetFileNameWithoutExtension(outputPath);
bool isResponse = filename.StartsWith("#", StringComparison.OrdinalIgnoreCase) &&
File.Exists(Path.Combine(Path.GetDirectoryName(outputPath),
Path.GetFileNameWithoutExtension(outputPath)));
TextReader resReader;
if(isResponse)
resReader = new StreamReader(Path.Combine(Path.GetDirectoryName(outputPath),
Path.GetFileNameWithoutExtension(outputPath)));
else
resReader = new StringReader(Path.GetFileNameWithoutExtension(outputPath));
if(isResponse)
eject = true;
PluginBase plugins = GetPluginBase.Instance;
List<IBaseWritableImage> candidates = new();
string extension = Path.GetExtension(outputPath);
// Try extension
if(string.IsNullOrEmpty(format))
candidates.AddRange(plugins.WritableImages.Values.Where(t => t.KnownExtensions.Contains(extension)));
// Try Id
else if(Guid.TryParse(format, out Guid outId))
candidates.AddRange(plugins.WritableImages.Values.Where(t => t.Id.Equals(outId)));
// Try name
else
candidates.AddRange(plugins.WritableImages.Values.Where(t => string.Equals(t.Name, format,
StringComparison.
InvariantCultureIgnoreCase)));
if(candidates.Count == 0)
{
AaruConsole.WriteLine("No plugin supports requested extension.");
return (int)ErrorNumber.FormatNotFound;
}
if(candidates.Count > 1)
{
AaruConsole.WriteLine("More than one plugin supports requested extension.");
return (int)ErrorNumber.TooManyFormats;
}
while(true)
{
string responseLine = resReader.ReadLine();
if(responseLine is null)
break;
if(responseLine.Any(c => c < 0x20))
{
AaruConsole.ErrorWriteLine("Invalid characters found in list of files, exiting...");
return (int)ErrorNumber.InvalidArgument;
}
if(isResponse)
{
AaruConsole.WriteLine("Please insert media with title {0} and press any key to continue...",
responseLine);
Console.ReadKey();
Thread.Sleep(1000);
}
responseLine = responseLine.Replace('/', '');
// Replace Windows forbidden filename characters with Japanese equivalents that are visually the same, but bigger.
if(DetectOS.IsWindows)
responseLine = responseLine.Replace('<', '\uFF1C').Replace('>', '\uFF1E').Replace(':', '\uFF1A').
Replace('"', '\u2033').Replace('\\', '').Replace('|', '').
Replace('?', '').Replace('*', '');
if(devicePath.Length == 2 &&
devicePath[1] == ':' &&
devicePath[0] != '/' &&
char.IsLetter(devicePath[0]))
devicePath = "\\\\.\\" + char.ToUpper(devicePath[0]) + ':';
Device dev = null;
ErrorNumber devErrno = ErrorNumber.NoError;
Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask("Opening device...").IsIndeterminate();
dev = Device.Create(devicePath, out devErrno);
});
switch(dev)
{
case null:
{
AaruConsole.ErrorWriteLine($"Could not open device, error {devErrno}.");
if(isResponse)
continue;
return (int)devErrno;
}
case Devices.Remote.Device remoteDev:
Statistics.AddRemote(remoteDev.RemoteApplication, remoteDev.RemoteVersion,
remoteDev.RemoteOperatingSystem, remoteDev.RemoteOperatingSystemVersion,
remoteDev.RemoteArchitecture);
break;
}
if(dev.Error)
{
AaruConsole.ErrorWriteLine(Error.Print(dev.LastError));
if(isResponse)
continue;
return (int)ErrorNumber.CannotOpenDevice;
}
Statistics.AddDevice(dev);
string outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), responseLine);
Resume resumeClass = null;
var xs = new XmlSerializer(typeof(Resume));
if(File.Exists(outputPrefix + ".resume.xml") && resume)
try
{
var sr = new StreamReader(outputPrefix + ".resume.xml");
resumeClass = (Resume)xs.Deserialize(sr);
sr.Close();
}
catch
{
AaruConsole.ErrorWriteLine("Incorrect resume file, not continuing...");
if(isResponse)
continue;
return (int)ErrorNumber.InvalidResume;
}
if(resumeClass != null &&
resumeClass.NextBlock > resumeClass.LastBlock &&
resumeClass.BadBlocks.Count == 0 &&
!resumeClass.Tape &&
(resumeClass.BadSubchannels is null || resumeClass.BadSubchannels.Count == 0) &&
(resumeClass.MissingTitleKeys is null || resumeClass.MissingTitleKeys.Count == 0))
{
AaruConsole.WriteLine("Media already dumped correctly, not continuing...");
if(isResponse)
continue;
return (int)ErrorNumber.AlreadyDumped;
}
CICMMetadataType sidecar = null;
var sidecarXs = new XmlSerializer(typeof(CICMMetadataType));
if(cicmXml != null)
if(File.Exists(cicmXml))
try
{
var sr = new StreamReader(cicmXml);
sidecar = (CICMMetadataType)sidecarXs.Deserialize(sr);
sr.Close();
}
catch
{
AaruConsole.ErrorWriteLine("Incorrect metadata sidecar file, not continuing...");
if(isResponse)
continue;
return (int)ErrorNumber.InvalidSidecar;
}
else
{
AaruConsole.ErrorWriteLine("Could not find metadata sidecar, not continuing...");
if(isResponse)
continue;
return (int)ErrorNumber.NoSuchFile;
}
plugins = GetPluginBase.Instance;
candidates = new List<IBaseWritableImage>();
// Try extension
if(string.IsNullOrEmpty(format))
candidates.AddRange(plugins.WritableImages.Values.Where(t =>
t.KnownExtensions.
Contains(Path.GetExtension(outputPath))));
// Try Id
else if(Guid.TryParse(format, out Guid outId))
candidates.AddRange(plugins.WritableImages.Values.Where(t => t.Id.Equals(outId)));
// Try name
else
candidates.AddRange(plugins.WritableImages.Values.Where(t => string.Equals(t.Name, format,
StringComparison.
InvariantCultureIgnoreCase)));
IBaseWritableImage outputFormat = candidates[0];
var dumpLog = new DumpLog(outputPrefix + ".log", dev, @private);
if(verbose)
{
dumpLog.WriteLine("Output image format: {0} ({1}).", outputFormat.Name, outputFormat.Id);
AaruConsole.VerboseWriteLine("Output image format: {0} ({1}).", outputFormat.Name, outputFormat.Id);
}
else
{
dumpLog.WriteLine("Output image format: {0}.", outputFormat.Name);
AaruConsole.WriteLine("Output image format: {0}.", outputFormat.Name);
}
var errorLog = new ErrorLog(outputPrefix + ".error.log");
var dumper = new Dump(resume, dev, devicePath, outputFormat, retryPasses, force, false, persistent,
stopOnError, resumeClass, dumpLog, encodingClass, outputPrefix,
outputPrefix + extension, parsedOptions, sidecar, skip, metadata, trim, firstPregap,
fixOffset, debug, wantedSubchannel, speed, @private, fixSubchannelPosition,
retrySubchannel, fixSubchannel, fixSubchannelCrc, skipCdiReadyHole, errorLog,
generateSubchannels, maxBlocks, useBufferedReads, storeEncrypted, titleKeys,
ignoreCdrRunOuts);
AnsiConsole.Progress().AutoClear(true).HideCompleted(true).
Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn()).
Start(ctx =>
{
dumper.UpdateStatus += text =>
{
AaruConsole.WriteLine(Markup.Escape(text));
};
dumper.ErrorMessage += text =>
{
AaruConsole.ErrorWriteLine($"[red]{Markup.Escape(text)}[/]");
};
dumper.StoppingErrorMessage += text =>
{
AaruConsole.ErrorWriteLine($"[red]{Markup.Escape(text)}[/]");
};
dumper.UpdateProgress += (text, current, maximum) =>
{
_progressTask1 ??= ctx.AddTask("Progress");
_progressTask1.Description = Markup.Escape(text);
_progressTask1.Value = current;
_progressTask1.MaxValue = maximum;
};
dumper.PulseProgress += text =>
{
if(_progressTask1 is null)
ctx.AddTask(Markup.Escape(text)).IsIndeterminate();
else
{
_progressTask1.Description = Markup.Escape(text);
_progressTask1.IsIndeterminate = true;
}
};
dumper.InitProgress += () =>
{
_progressTask1 = ctx.AddTask("Progress");
};
dumper.EndProgress += () =>
{
_progressTask1?.StopTask();
_progressTask1 = null;
};
dumper.InitProgress2 += () =>
{
_progressTask2 = ctx.AddTask("Progress");
};
dumper.EndProgress2 += () =>
{
_progressTask2?.StopTask();
_progressTask2 = null;
};
dumper.UpdateProgress2 += (text, current, maximum) =>
{
_progressTask2 ??= ctx.AddTask("Progress");
_progressTask2.Description = Markup.Escape(text);
_progressTask2.Value = current;
_progressTask2.MaxValue = maximum;
};
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
dumper.Abort();
};
dumper.Start();
});
if(eject && dev.IsRemovable)
Spectre.ProgressSingleSpinner(ctx =>
{
ctx.AddTask("Ejecting media...").IsIndeterminate();
switch(dev.Type)
{
case DeviceType.ATA:
dev.DoorUnlock(out _, dev.Timeout, out _);
dev.MediaEject(out _, dev.Timeout, out _);
break;
case DeviceType.ATAPI:
case DeviceType.SCSI:
switch(dev.ScsiType)
{
case PeripheralDeviceTypes.DirectAccess:
case PeripheralDeviceTypes.SimplifiedDevice:
case PeripheralDeviceTypes.SCSIZonedBlockDevice:
case PeripheralDeviceTypes.WriteOnceDevice:
case PeripheralDeviceTypes.OpticalDevice:
case PeripheralDeviceTypes.OCRWDevice:
dev.SpcAllowMediumRemoval(out _, dev.Timeout, out _);
dev.EjectTray(out _, dev.Timeout, out _);
break;
case PeripheralDeviceTypes.MultiMediaDevice:
dev.AllowMediumRemoval(out _, dev.Timeout, out _);
dev.EjectTray(out _, dev.Timeout, out _);
break;
case PeripheralDeviceTypes.SequentialAccess:
dev.SpcAllowMediumRemoval(out _, dev.Timeout, out _);
dev.LoadUnload(out _, true, false, false, false, false, dev.Timeout, out _);
break;
}
break;
}
});
dev.Close();
}
return (int)ErrorNumber.NoError;
}
}