diff --git a/.idea/.idea.DiscImageChef/.idea/contentModel.xml b/.idea/.idea.DiscImageChef/.idea/contentModel.xml
index 499afadd8..dd6544bf1 100644
--- a/.idea/.idea.DiscImageChef/.idea/contentModel.xml
+++ b/.idea/.idea.DiscImageChef/.idea/contentModel.xml
@@ -33,6 +33,7 @@
+
@@ -49,7 +50,6 @@
-
diff --git a/DiscImageChef/Commands/Analyze.cs b/DiscImageChef/Commands/Analyze.cs
index b700dc384..2e9a8a557 100644
--- a/DiscImageChef/Commands/Analyze.cs
+++ b/DiscImageChef/Commands/Analyze.cs
@@ -37,41 +37,92 @@ using DiscImageChef.CommonTypes;
using DiscImageChef.CommonTypes.Interfaces;
using DiscImageChef.Console;
using DiscImageChef.Core;
+using Mono.Options;
namespace DiscImageChef.Commands
{
- static class Analyze
+ class AnalyzeCommand : Command
{
- internal static void DoAnalyze(AnalyzeOptions options)
+ string encodingName;
+ string inputFile;
+ bool searchForFilesystems = true;
+ bool searchForPartitions = true;
+ bool showHelp;
+
+ public AnalyzeCommand() : base("analyze",
+ "Analyzes a disc image and searches for partitions and/or filesystems.")
{
- DicConsole.DebugWriteLine("Analyze command", "--debug={0}", options.Debug);
- DicConsole.DebugWriteLine("Analyze command", "--verbose={0}", options.Verbose);
- DicConsole.DebugWriteLine("Analyze command", "--input={0}", options.InputFile);
- DicConsole.DebugWriteLine("Analyze command", "--filesystems={0}", options.SearchForFilesystems);
- DicConsole.DebugWriteLine("Analyze command", "--partitions={0}", options.SearchForPartitions);
- DicConsole.DebugWriteLine("Analyze command", "--encoding={0}", options.EncodingName);
+ Options = new OptionSet
+ {
+ $"{MainClass.AssemblyTitle} {MainClass.AssemblyVersion?.InformationalVersion}",
+ $"{MainClass.AssemblyCopyright}",
+ "",
+ $"usage: DiscImageChef {Name} [OPTIONS] imagefile",
+ "",
+ Help,
+ {"encoding|e=", "Name of character encoding to use.", s => encodingName = s},
+ {"filesystems|f", "Searches and analyzes filesystems.", b => searchForFilesystems = b != null},
+ {"partitions|p", "Searches and interprets partitions.", b => searchForPartitions = b != null},
+ {"help|h|?", "Show this message and exit.", v => showHelp = v != null}
+ };
+ }
+
+ public override int Invoke(IEnumerable arguments)
+ {
+ List extra = Options.Parse(arguments);
+
+ if(showHelp)
+ {
+ Options.WriteOptionDescriptions(CommandSet.Out);
+ return 0;
+ }
+
+ MainClass.PrintCopyright();
+ if(MainClass.Debug) DicConsole.DebugWriteLineEvent += System.Console.Error.WriteLine;
+ if(MainClass.Verbose) DicConsole.VerboseWriteLineEvent += System.Console.WriteLine;
+
+ if(extra.Count > 1)
+ {
+ DicConsole.ErrorWriteLine("Too many arguments.");
+ return 1;
+ }
+
+ if(extra.Count == 0)
+ {
+ DicConsole.ErrorWriteLine("Missing input image.");
+ return 1;
+ }
+
+ inputFile = extra[0];
+
+ DicConsole.DebugWriteLine("Analyze command", "--debug={0}", MainClass.Debug);
+ DicConsole.DebugWriteLine("Analyze command", "--encoding={0}", encodingName);
+ DicConsole.DebugWriteLine("Analyze command", "--filesystems={0}", searchForFilesystems);
+ DicConsole.DebugWriteLine("Analyze command", "--input={0}", inputFile);
+ DicConsole.DebugWriteLine("Analyze command", "--partitions={0}", searchForPartitions);
+ DicConsole.DebugWriteLine("Analyze command", "--verbose={0}", MainClass.Verbose);
FiltersList filtersList = new FiltersList();
- IFilter inputFilter = filtersList.GetFilter(options.InputFile);
+ IFilter inputFilter = filtersList.GetFilter(inputFile);
if(inputFilter == null)
{
DicConsole.ErrorWriteLine("Cannot open specified file.");
- return;
+ return 2;
}
Encoding encoding = null;
- if(options.EncodingName != null)
+ if(encodingName != null)
try
{
- encoding = Claunia.Encoding.Encoding.GetEncoding(options.EncodingName);
- if(options.Verbose) DicConsole.VerboseWriteLine("Using encoding for {0}.", encoding.EncodingName);
+ encoding = Claunia.Encoding.Encoding.GetEncoding(encodingName);
+ if(MainClass.Verbose) DicConsole.VerboseWriteLine("Using encoding for {0}.", encoding.EncodingName);
}
catch(ArgumentException)
{
DicConsole.ErrorWriteLine("Specified encoding is not supported.");
- return;
+ return 5;
}
PluginBase plugins = GetPluginBase.Instance;
@@ -85,10 +136,10 @@ namespace DiscImageChef.Commands
if(imageFormat == null)
{
DicConsole.WriteLine("Image format not identified, not proceeding with analysis.");
- return;
+ return 3;
}
- if(options.Verbose)
+ if(MainClass.Verbose)
DicConsole.VerboseWriteLine("Image format identified by {0} ({1}).", imageFormat.Name,
imageFormat.Id);
else DicConsole.WriteLine("Image format identified by {0}.", imageFormat.Name);
@@ -100,31 +151,31 @@ namespace DiscImageChef.Commands
{
DicConsole.WriteLine("Unable to open image format");
DicConsole.WriteLine("No error given");
- return;
+ return 4;
}
- if(options.Verbose)
+ if(MainClass.Verbose)
{
- Core.ImageInfo.PrintImageInfo(imageFormat);
+ ImageInfo.PrintImageInfo(imageFormat);
DicConsole.WriteLine();
}
- Core.Statistics.AddMediaFormat(imageFormat.Format);
- Core.Statistics.AddMedia(imageFormat.Info.MediaType, false);
- Core.Statistics.AddFilter(inputFilter.Name);
+ Statistics.AddMediaFormat(imageFormat.Format);
+ Statistics.AddMedia(imageFormat.Info.MediaType, false);
+ Statistics.AddFilter(inputFilter.Name);
}
catch(Exception ex)
{
DicConsole.ErrorWriteLine("Unable to open image format");
DicConsole.ErrorWriteLine("Error: {0}", ex.Message);
DicConsole.DebugWriteLine("Analyze command", "Stack trace: {0}", ex.StackTrace);
- return;
+ return -1;
}
List idPlugins;
IFilesystem plugin;
string information;
- if(options.SearchForPartitions)
+ if(searchForPartitions)
{
List partitions = Core.Partitions.GetAll(imageFormat);
Core.Partitions.AddSchemesToStats(partitions);
@@ -132,10 +183,10 @@ namespace DiscImageChef.Commands
if(partitions.Count == 0)
{
DicConsole.DebugWriteLine("Analyze command", "No partitions found");
- if(!options.SearchForFilesystems)
+ if(!searchForFilesystems)
{
DicConsole.WriteLine("No partitions founds, not searching for filesystems");
- return;
+ return -2;
}
checkraw = true;
@@ -158,7 +209,7 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine("Partition description:");
DicConsole.WriteLine(partitions[i].Description);
- if(!options.SearchForFilesystems) continue;
+ if(!searchForFilesystems) continue;
DicConsole.WriteLine("Identifying filesystem on partition");
@@ -174,7 +225,7 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine($"As identified by {plugin.Name}.");
plugin.GetInformation(imageFormat, partitions[i], out information, encoding);
DicConsole.Write(information);
- Core.Statistics.AddFilesystem(plugin.XmlFsType.Type);
+ Statistics.AddFilesystem(plugin.XmlFsType.Type);
}
}
else
@@ -185,7 +236,7 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine($"Identified by {plugin.Name}.");
plugin.GetInformation(imageFormat, partitions[i], out information, encoding);
DicConsole.Write("{0}", information);
- Core.Statistics.AddFilesystem(plugin.XmlFsType.Type);
+ Statistics.AddFilesystem(plugin.XmlFsType.Type);
}
}
}
@@ -212,7 +263,7 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine($"As identified by {plugin.Name}.");
plugin.GetInformation(imageFormat, wholePart, out information, encoding);
DicConsole.Write(information);
- Core.Statistics.AddFilesystem(plugin.XmlFsType.Type);
+ Statistics.AddFilesystem(plugin.XmlFsType.Type);
}
}
else
@@ -223,7 +274,7 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine($"Identified by {plugin.Name}.");
plugin.GetInformation(imageFormat, wholePart, out information, encoding);
DicConsole.Write(information);
- Core.Statistics.AddFilesystem(plugin.XmlFsType.Type);
+ Statistics.AddFilesystem(plugin.XmlFsType.Type);
}
}
}
@@ -234,7 +285,8 @@ namespace DiscImageChef.Commands
DicConsole.DebugWriteLine("Analyze command", ex.StackTrace);
}
- Core.Statistics.AddCommand("analyze");
+ Statistics.AddCommand("analyze");
+ return 0;
}
}
}
\ No newline at end of file
diff --git a/DiscImageChef/Commands/Benchmark.cs b/DiscImageChef/Commands/Benchmark.cs
index 1d3a0762e..fd2363dc0 100644
--- a/DiscImageChef/Commands/Benchmark.cs
+++ b/DiscImageChef/Commands/Benchmark.cs
@@ -33,19 +33,60 @@
using System.Collections.Generic;
using DiscImageChef.Console;
using DiscImageChef.Core;
+using Mono.Options;
namespace DiscImageChef.Commands
{
- static class Benchmark
+ class BenchmarkCommand : Command
{
- internal static void DoBenchmark(BenchmarkOptions options)
- {
- Dictionary checksumTimes = new Dictionary();
- Core.Benchmark.InitProgressEvent += Progress.InitProgress;
- Core.Benchmark.UpdateProgressEvent += Progress.UpdateProgress;
- Core.Benchmark.EndProgressEvent += Progress.EndProgress;
+ int blockSize = 512;
+ int bufferSize = 128;
+ bool showHelp;
- BenchmarkResults results = Core.Benchmark.Do(options.BufferSize * 1024 * 1024, options.BlockSize);
+ public BenchmarkCommand() : base("benchmark", "Benchmarks hashing and entropy calculation.")
+ {
+ Options = new OptionSet
+ {
+ $"{MainClass.AssemblyTitle} {MainClass.AssemblyVersion?.InformationalVersion}",
+ $"{MainClass.AssemblyCopyright}",
+ "",
+ $"usage: DiscImageChef {Name} [OPTIONS]",
+ "",
+ Help,
+ {"block-size|b=", "Block size.", (int i) => blockSize = i},
+ {"buffer-size|s=", "Buffer size in mebibytes.", (int i) => bufferSize = i},
+ {"help|h|?", "Show this message and exit.", v => showHelp = v != null}
+ };
+ }
+
+ public override int Invoke(IEnumerable arguments)
+ {
+ List extra = Options.Parse(arguments);
+
+ if(showHelp)
+ {
+ Options.WriteOptionDescriptions(CommandSet.Out);
+ return 0;
+ }
+
+ MainClass.PrintCopyright();
+ if(MainClass.Debug) DicConsole.DebugWriteLineEvent += System.Console.Error.WriteLine;
+ if(MainClass.Verbose) DicConsole.VerboseWriteLineEvent += System.Console.WriteLine;
+
+ if(extra.Count != 0)
+ {
+ DicConsole.ErrorWriteLine("Too many arguments.");
+ return 1;
+ }
+
+ DicConsole.DebugWriteLine("Benchmark command", "--debug={0}", MainClass.Debug);
+ DicConsole.DebugWriteLine("Benchmark command", "--verbose={0}", MainClass.Verbose);
+
+ Benchmark.InitProgressEvent += Progress.InitProgress;
+ Benchmark.UpdateProgressEvent += Progress.UpdateProgress;
+ Benchmark.EndProgressEvent += Progress.EndProgress;
+
+ BenchmarkResults results = Benchmark.Do(bufferSize * 1024 * 1024, blockSize);
DicConsole.WriteLine("Took {0} seconds to fill buffer, {1:F3} MiB/sec.", results.FillTime,
results.FillSpeed);
@@ -55,11 +96,8 @@ namespace DiscImageChef.Commands
results.EntropySpeed);
foreach(KeyValuePair entry in results.Entries)
- {
- checksumTimes.Add(entry.Key, entry.Value.TimeSpan);
DicConsole.WriteLine("Took {0} seconds to {1} buffer, {2:F3} MiB/sec.", entry.Value.TimeSpan, entry.Key,
entry.Value.Speed);
- }
DicConsole.WriteLine("Took {0} seconds to do all algorithms at the same time, {1:F3} MiB/sec.",
results.TotalTime, results.TotalSpeed);
@@ -70,7 +108,8 @@ namespace DiscImageChef.Commands
DicConsole.WriteLine("Max memory used is {0} bytes", results.MaxMemory);
DicConsole.WriteLine("Min memory used is {0} bytes", results.MinMemory);
- Core.Statistics.AddCommand("benchmark");
+ Statistics.AddCommand("benchmark");
+ return 0;
}
}
}
\ No newline at end of file
diff --git a/DiscImageChef/Commands/Checksum.cs b/DiscImageChef/Commands/Checksum.cs
index 1631c231d..c60301a0c 100644
--- a/DiscImageChef/Commands/Checksum.cs
+++ b/DiscImageChef/Commands/Checksum.cs
@@ -37,43 +37,117 @@ using DiscImageChef.CommonTypes.Interfaces;
using DiscImageChef.CommonTypes.Structs;
using DiscImageChef.Console;
using DiscImageChef.Core;
+using Mono.Options;
using Schemas;
namespace DiscImageChef.Commands
{
- static class Checksum
+ class ChecksumCommand : Command
{
// How many sectors to read at once
const uint SECTORS_TO_READ = 256;
- internal static void DoChecksum(ChecksumOptions options)
+ bool doAdler32 = true;
+ bool doCrc16 = true;
+ bool doCrc32 = true;
+ bool doCrc64;
+ bool doFletcher16;
+ bool doFletcher32;
+ bool doMd5 = true;
+ bool doRipemd160;
+ bool doSha1 = true;
+ bool doSha256;
+ bool doSha384;
+ bool doSha512;
+ bool doSpamSum = true;
+ string inputFile;
+ bool separatedTracks = true;
+ bool showHelp;
+ bool wholeDisc = true;
+
+ public ChecksumCommand() : base("checksum", "Checksums an image.")
{
- DicConsole.DebugWriteLine("Checksum command", "--debug={0}", options.Debug);
- DicConsole.DebugWriteLine("Checksum command", "--verbose={0}", options.Verbose);
- DicConsole.DebugWriteLine("Checksum command", "--separated-tracks={0}", options.SeparatedTracks);
- DicConsole.DebugWriteLine("Checksum command", "--whole-disc={0}", options.WholeDisc);
- DicConsole.DebugWriteLine("Checksum command", "--input={0}", options.InputFile);
- DicConsole.DebugWriteLine("Checksum command", "--adler32={0}", options.DoAdler32);
- DicConsole.DebugWriteLine("Checksum command", "--crc16={0}", options.DoCrc16);
- DicConsole.DebugWriteLine("Checksum command", "--crc32={0}", options.DoCrc32);
- DicConsole.DebugWriteLine("Checksum command", "--crc64={0}", options.DoCrc64);
- DicConsole.DebugWriteLine("Checksum command", "--md5={0}", options.DoMd5);
- DicConsole.DebugWriteLine("Checksum command", "--ripemd160={0}", options.DoRipemd160);
- DicConsole.DebugWriteLine("Checksum command", "--sha1={0}", options.DoSha1);
- DicConsole.DebugWriteLine("Checksum command", "--sha256={0}", options.DoSha256);
- DicConsole.DebugWriteLine("Checksum command", "--sha384={0}", options.DoSha384);
- DicConsole.DebugWriteLine("Checksum command", "--sha512={0}", options.DoSha512);
- DicConsole.DebugWriteLine("Checksum command", "--spamsum={0}", options.DoSpamSum);
- DicConsole.DebugWriteLine("Checksum command", "--fletcher16={0}", options.DoFletcher16);
- DicConsole.DebugWriteLine("Checksum command", "--fletcher32={0}", options.DoFletcher32);
+ Options = new OptionSet
+ {
+ $"{MainClass.AssemblyTitle} {MainClass.AssemblyVersion?.InformationalVersion}",
+ $"{MainClass.AssemblyCopyright}",
+ "",
+ $"usage: DiscImageChef {Name} [OPTIONS] imagefile",
+ "",
+ Help,
+ {"adler32|a", "Calculates Adler-32.", b => doAdler32 = b != null},
+ {"crc16", "Calculates CRC16.", b => doCrc16 = b != null},
+ {"crc32|c", "Calculates CRC32.", b => doCrc32 = b != null},
+ {"crc64", "Calculates CRC64 (ECMA).", b => doCrc64 = b != null},
+ {"fletcher16", "Calculates Fletcher-16.", b => doFletcher16 = b != null},
+ {"fletcher32", "Calculates Fletcher-32.", b => doFletcher32 = b != null},
+ {"md5|m", "Calculates MD5.", b => doMd5 = b != null},
+ {"ripemd160", "Calculates RIPEMD160.", b => doRipemd160 = b != null},
+ {"separated-tracks|t", "Checksums each track separately.", b => separatedTracks = b != null},
+ {"sha1|s", "Calculates SHA1.", b => doSha1 = b != null},
+ {"sha256", "Calculates SHA256.", b => doSha256 = b != null},
+ {"sha384", "Calculates SHA384.", b => doSha384 = b != null},
+ {"sha512", "Calculates SHA512.", b => doSha512 = b != null},
+ {"spamsum|f", "Calculates SpamSum fuzzy hash.", b => doSpamSum = b != null},
+ {"whole-disc|w", "Checksums the whole disc.", b => wholeDisc = b != null},
+ {"help|h|?", "Show this message and exit.", v => showHelp = v != null}
+ };
+ }
+
+ public override int Invoke(IEnumerable arguments)
+ {
+ List extra = Options.Parse(arguments);
+
+ if(showHelp)
+ {
+ Options.WriteOptionDescriptions(CommandSet.Out);
+ return 0;
+ }
+
+ MainClass.PrintCopyright();
+ if(MainClass.Debug) DicConsole.DebugWriteLineEvent += System.Console.Error.WriteLine;
+ if(MainClass.Verbose) DicConsole.VerboseWriteLineEvent += System.Console.WriteLine;
+
+ if(extra.Count > 1)
+ {
+ DicConsole.ErrorWriteLine("Too many arguments.");
+ return 1;
+ }
+
+ if(extra.Count == 0)
+ {
+ DicConsole.ErrorWriteLine("Missing input image.");
+ return 1;
+ }
+
+ inputFile = extra[0];
+
+ DicConsole.DebugWriteLine("Checksum command", "--adler32={0}", doAdler32);
+ DicConsole.DebugWriteLine("Checksum command", "--crc16={0}", doCrc16);
+ DicConsole.DebugWriteLine("Checksum command", "--crc32={0}", doCrc32);
+ DicConsole.DebugWriteLine("Checksum command", "--crc64={0}", doCrc64);
+ DicConsole.DebugWriteLine("Checksum command", "--debug={0}", MainClass.Debug);
+ DicConsole.DebugWriteLine("Checksum command", "--fletcher16={0}", doFletcher16);
+ DicConsole.DebugWriteLine("Checksum command", "--fletcher32={0}", doFletcher32);
+ DicConsole.DebugWriteLine("Checksum command", "--input={0}", inputFile);
+ DicConsole.DebugWriteLine("Checksum command", "--md5={0}", doMd5);
+ DicConsole.DebugWriteLine("Checksum command", "--ripemd160={0}", doRipemd160);
+ DicConsole.DebugWriteLine("Checksum command", "--separated-tracks={0}", separatedTracks);
+ DicConsole.DebugWriteLine("Checksum command", "--sha1={0}", doSha1);
+ DicConsole.DebugWriteLine("Checksum command", "--sha256={0}", doSha256);
+ DicConsole.DebugWriteLine("Checksum command", "--sha384={0}", doSha384);
+ DicConsole.DebugWriteLine("Checksum command", "--sha512={0}", doSha512);
+ DicConsole.DebugWriteLine("Checksum command", "--spamsum={0}", doSpamSum);
+ DicConsole.DebugWriteLine("Checksum command", "--verbose={0}", MainClass.Verbose);
+ DicConsole.DebugWriteLine("Checksum command", "--whole-disc={0}", wholeDisc);
FiltersList filtersList = new FiltersList();
- IFilter inputFilter = filtersList.GetFilter(options.InputFile);
+ IFilter inputFilter = filtersList.GetFilter(inputFile);
if(inputFilter == null)
{
DicConsole.ErrorWriteLine("Cannot open specified file.");
- return;
+ return 1;
}
IMediaImage inputFormat = ImageFormat.Detect(inputFilter);
@@ -81,44 +155,44 @@ namespace DiscImageChef.Commands
if(inputFormat == null)
{
DicConsole.ErrorWriteLine("Unable to recognize image format, not checksumming");
- return;
+ return 2;
}
inputFormat.Open(inputFilter);
- Core.Statistics.AddMediaFormat(inputFormat.Format);
- Core.Statistics.AddMedia(inputFormat.Info.MediaType, false);
- Core.Statistics.AddFilter(inputFilter.Name);
+ Statistics.AddMediaFormat(inputFormat.Format);
+ Statistics.AddMedia(inputFormat.Info.MediaType, false);
+ Statistics.AddFilter(inputFilter.Name);
EnableChecksum enabledChecksums = new EnableChecksum();
- if(options.DoAdler32) enabledChecksums |= EnableChecksum.Adler32;
- if(options.DoCrc16) enabledChecksums |= EnableChecksum.Crc16;
- if(options.DoCrc32) enabledChecksums |= EnableChecksum.Crc32;
- if(options.DoCrc64) enabledChecksums |= EnableChecksum.Crc64;
- if(options.DoMd5) enabledChecksums |= EnableChecksum.Md5;
- if(options.DoRipemd160) enabledChecksums |= EnableChecksum.Ripemd160;
- if(options.DoSha1) enabledChecksums |= EnableChecksum.Sha1;
- if(options.DoSha256) enabledChecksums |= EnableChecksum.Sha256;
- if(options.DoSha384) enabledChecksums |= EnableChecksum.Sha384;
- if(options.DoSha512) enabledChecksums |= EnableChecksum.Sha512;
- if(options.DoSpamSum) enabledChecksums |= EnableChecksum.SpamSum;
- if(options.DoFletcher16) enabledChecksums |= EnableChecksum.Fletcher16;
- if(options.DoFletcher32) enabledChecksums |= EnableChecksum.Fletcher32;
+ if(doAdler32) enabledChecksums |= EnableChecksum.Adler32;
+ if(doCrc16) enabledChecksums |= EnableChecksum.Crc16;
+ if(doCrc32) enabledChecksums |= EnableChecksum.Crc32;
+ if(doCrc64) enabledChecksums |= EnableChecksum.Crc64;
+ if(doMd5) enabledChecksums |= EnableChecksum.Md5;
+ if(doRipemd160) enabledChecksums |= EnableChecksum.Ripemd160;
+ if(doSha1) enabledChecksums |= EnableChecksum.Sha1;
+ if(doSha256) enabledChecksums |= EnableChecksum.Sha256;
+ if(doSha384) enabledChecksums |= EnableChecksum.Sha384;
+ if(doSha512) enabledChecksums |= EnableChecksum.Sha512;
+ if(doSpamSum) enabledChecksums |= EnableChecksum.SpamSum;
+ if(doFletcher16) enabledChecksums |= EnableChecksum.Fletcher16;
+ if(doFletcher32) enabledChecksums |= EnableChecksum.Fletcher32;
- Core.Checksum mediaChecksum = null;
+ Checksum mediaChecksum = null;
if(inputFormat.Info.HasPartitions)
try
{
- Core.Checksum trackChecksum = null;
+ Checksum trackChecksum = null;
- if(options.WholeDisc) mediaChecksum = new Core.Checksum(enabledChecksums);
+ if(wholeDisc) mediaChecksum = new Checksum(enabledChecksums);
ulong previousTrackEnd = 0;
List