|  | @@ -0,0 +1,284 @@
 | 
	
		
			
				|  |  | +using System;
 | 
	
		
			
				|  |  | +using System.Collections.Generic;
 | 
	
		
			
				|  |  | +using System.IO;
 | 
	
		
			
				|  |  | +using System.IO.Abstractions;
 | 
	
		
			
				|  |  | +using System.Linq;
 | 
	
		
			
				|  |  | +using System.Text;
 | 
	
		
			
				|  |  | +using CommandLine;
 | 
	
		
			
				|  |  | +using COM3D2.Toolkit.Arc;
 | 
	
		
			
				|  |  | +using Ganss.IO;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +namespace ArcToolkitCLI
 | 
	
		
			
				|  |  | +{
 | 
	
		
			
				|  |  | +    internal interface IOptions
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        [Value(0, MetaName = "input", HelpText = "Input ARC files")]
 | 
	
		
			
				|  |  | +        IEnumerable<string> Input { get; set; }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    internal interface IDecryptionOptions
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        [Option("arc-search-dir", Required = false, HelpText = "Directory where to search ARC file to decrypt the WARP file")]
 | 
	
		
			
				|  |  | +        string ArcDirectory { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [Option('k', "key", HelpText = "Decryption key as a base64 string (applied to all inputs)", Required = false)]
 | 
	
		
			
				|  |  | +        string DecryptionKey { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [Option("key-file",
 | 
	
		
			
				|  |  | +                HelpText = "A file with decryption keys on each line. Format of the file is <decryption arc name>:<key in base64>.",
 | 
	
		
			
				|  |  | +                Required = false)]
 | 
	
		
			
				|  |  | +        string KeyFile { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [Option("warc", Required = false, HelpText = "WARC file to use for decryption")]
 | 
	
		
			
				|  |  | +        string WarcFile { get; set; }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [Verb("extract", HelpText = "Extract the contents of the given ARC files")]
 | 
	
		
			
				|  |  | +    internal class ExtractOptions : IOptions, IDecryptionOptions
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        [Option('o', "output", HelpText = "Output directory", Required = true)]
 | 
	
		
			
				|  |  | +        public string Output { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public string ArcDirectory { get; set; }
 | 
	
		
			
				|  |  | +        public string DecryptionKey { get; set; }
 | 
	
		
			
				|  |  | +        public string KeyFile { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public string WarcFile { get; set; }
 | 
	
		
			
				|  |  | +        public IEnumerable<string> Input { get; set; }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [Verb("info", HelpText = "Display information about the given ARC files")]
 | 
	
		
			
				|  |  | +    internal class InfoOptions : IOptions
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        [Option("only-key",
 | 
	
		
			
				|  |  | +                HelpText = "If the archive is a WARC file, output only the decryption key as a base64 string",
 | 
	
		
			
				|  |  | +                Required = false)]
 | 
	
		
			
				|  |  | +        public bool OnlyKey { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public IEnumerable<string> Input { get; set; }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [Verb("decrypt", HelpText = "Decrypts the provided WARP files")]
 | 
	
		
			
				|  |  | +    internal class DecryptOptions : IOptions, IDecryptionOptions
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        [Option('o', "output", HelpText = "Output directory", Default = ".")]
 | 
	
		
			
				|  |  | +        public string Output { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public string ArcDirectory { get; set; }
 | 
	
		
			
				|  |  | +        public string DecryptionKey { get; set; }
 | 
	
		
			
				|  |  | +        public string KeyFile { get; set; }
 | 
	
		
			
				|  |  | +        public string WarcFile { get; set; }
 | 
	
		
			
				|  |  | +        public IEnumerable<string> Input { get; set; }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    internal static class Errors
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        public enum ErrorCodes
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            NotAFile = 100,
 | 
	
		
			
				|  |  | +            UnknownArcType,
 | 
	
		
			
				|  |  | +            KeyNotFound,
 | 
	
		
			
				|  |  | +            InvalidKeyFile,
 | 
	
		
			
				|  |  | +            NoCorrectKey
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public static readonly string[] ErrorMessages =
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +                "{0} is not a file",
 | 
	
		
			
				|  |  | +                "{0} is not a known ARC file",
 | 
	
		
			
				|  |  | +                "No key specified or the key file does not exist",
 | 
	
		
			
				|  |  | +                "The provided keyfile is not valid",
 | 
	
		
			
				|  |  | +                "The ARC {0} needs a key from {1}"
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public static int Error(ErrorCodes code, params object[] args)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            Console.Error.WriteLine(ErrorMessages[code - ErrorCodes.NotAFile], args);
 | 
	
		
			
				|  |  | +            return (int) code;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    internal static class Util
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        private static readonly char[] GlobCharacters = {'*', '{', '}', '[', ']', '?'};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public static IEnumerable<FileSystemInfoBase> EnumerateFiles(IEnumerable<string> patterns)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            foreach (string pattern in patterns)
 | 
	
		
			
				|  |  | +                if (IsGlob(pattern))
 | 
	
		
			
				|  |  | +                    foreach (var fileSystemInfoBase in Glob.Expand(pattern))
 | 
	
		
			
				|  |  | +                        yield return fileSystemInfoBase;
 | 
	
		
			
				|  |  | +                else
 | 
	
		
			
				|  |  | +                    yield return new FileInfoWrapper(new FileSystem(), new FileInfo(pattern));
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private static bool IsGlob(string path)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            return path.IndexOfAny(GlobCharacters) >= 0;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    internal class Program
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        private static int Main(string[] args)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            return Parser.Default.ParseArguments<InfoOptions, ExtractOptions, DecryptOptions>(args)
 | 
	
		
			
				|  |  | +                         .MapResult((InfoOptions opts) => DisplayInfo(opts),
 | 
	
		
			
				|  |  | +                                    (ExtractOptions opts) => Extract(opts),
 | 
	
		
			
				|  |  | +                                    (DecryptOptions opts) => Decrypt(opts),
 | 
	
		
			
				|  |  | +                                    errs => 1);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private static int DisplayInfo(InfoOptions opts)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            bool first = true;
 | 
	
		
			
				|  |  | +            foreach (var file in Util.EnumerateFiles(opts.Input))
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                if (!file.Exists)
 | 
	
		
			
				|  |  | +                    return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if (!first && !opts.OnlyKey)
 | 
	
		
			
				|  |  | +                    Console.WriteLine();
 | 
	
		
			
				|  |  | +                first = false;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                using (var stream = File.OpenRead(file.FullName))
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var header = new byte[4];
 | 
	
		
			
				|  |  | +                    stream.Read(header, 0, header.Length);
 | 
	
		
			
				|  |  | +                    string headerString = Encoding.ASCII.GetString(header);
 | 
	
		
			
				|  |  | +                    stream.Position = 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    if (headerString == "warc")
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        var key = new byte[2048];
 | 
	
		
			
				|  |  | +                        stream.Read(key, 0, key.Length);
 | 
	
		
			
				|  |  | +                        string keyBase64 = Convert.ToBase64String(key);
 | 
	
		
			
				|  |  | +                        if (opts.OnlyKey)
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"{Path.GetFileName(file.FullName)}:{keyBase64}");
 | 
	
		
			
				|  |  | +                            continue;
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                        stream.Position = 0;
 | 
	
		
			
				|  |  | +                        using (var warc = new WarcArc(stream))
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"File name: {file.Name}");
 | 
	
		
			
				|  |  | +                            Console.WriteLine("ARC type: WARC");
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"ARC Name: {warc.Name}");
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"File count: {warc.Entries.Count()}");
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"Decryption key: {keyBase64}");
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    else if (headerString == "warp")
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        if (opts.OnlyKey)
 | 
	
		
			
				|  |  | +                            continue;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                        Console.WriteLine($"File name: {file.Name}");
 | 
	
		
			
				|  |  | +                        Console.WriteLine("ARC type: WARP");
 | 
	
		
			
				|  |  | +                        using (var br = new BinaryReader(stream))
 | 
	
		
			
				|  |  | +                            Console.WriteLine($"Needs a decryption key from the following ARC: {WarpArc.GetKeyWarpName(br)}");
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    else
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return 0;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private static int Extract(ExtractOptions opts)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            return 0;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private static byte[] ReadKeyFromFile(string filename)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            using (var br = new BinaryReader(File.OpenRead(filename)))
 | 
	
		
			
				|  |  | +                return br.ReadBytes(2048);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private static int Decrypt(DecryptOptions opts)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            var keysDict = new Dictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
 | 
	
		
			
				|  |  | +            if (!string.IsNullOrWhiteSpace(opts.KeyFile))
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                if (!File.Exists(opts.KeyFile))
 | 
	
		
			
				|  |  | +                    return Errors.Error(Errors.ErrorCodes.KeyNotFound);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                foreach (string line in File.ReadAllLines(opts.KeyFile))
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var parts = line.Split(':');
 | 
	
		
			
				|  |  | +                    if (parts.Length != 2)
 | 
	
		
			
				|  |  | +                        return Errors.Error(Errors.ErrorCodes.InvalidKeyFile);
 | 
	
		
			
				|  |  | +                    keysDict[parts[0].Trim()] = Convert.FromBase64String(parts[1].Trim());
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            else if (!string.IsNullOrWhiteSpace(opts.DecryptionKey))
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                keysDict["*"] = Convert.FromBase64String(opts.DecryptionKey);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            else if (string.IsNullOrWhiteSpace(opts.ArcDirectory) && string.IsNullOrWhiteSpace(opts.WarcFile))
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                return Errors.Error(Errors.ErrorCodes.KeyNotFound);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Directory.CreateDirectory(opts.Output);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            foreach (var file in Util.EnumerateFiles(opts.Input))
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                if (!file.Exists)
 | 
	
		
			
				|  |  | +                    return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                using (var stream = File.OpenRead(file.FullName))
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var header = new byte[4];
 | 
	
		
			
				|  |  | +                    stream.Read(header, 0, header.Length);
 | 
	
		
			
				|  |  | +                    string headerString = Encoding.ASCII.GetString(header);
 | 
	
		
			
				|  |  | +                    stream.Position = 0;
 | 
	
		
			
				|  |  | +                    if (headerString == "warc")
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        Console.WriteLine($"{file.Name} is a WARC file, skipping...");
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    else if (headerString == "warp")
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        try
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            var arcStream = WarpArc.DecryptWarp(stream,
 | 
	
		
			
				|  |  | +                                                                requestedFile =>
 | 
	
		
			
				|  |  | +                                                                {
 | 
	
		
			
				|  |  | +                                                                    if (keysDict.TryGetValue("*", out var key))
 | 
	
		
			
				|  |  | +                                                                        return key;
 | 
	
		
			
				|  |  | +                                                                    if (keysDict.TryGetValue(requestedFile, out key))
 | 
	
		
			
				|  |  | +                                                                        return key;
 | 
	
		
			
				|  |  | +                                                                    if (Directory.Exists(opts.ArcDirectory)
 | 
	
		
			
				|  |  | +                                                                     && File.Exists(Path.Combine(opts.ArcDirectory, requestedFile)))
 | 
	
		
			
				|  |  | +                                                                        return ReadKeyFromFile(Path.Combine(opts.ArcDirectory, requestedFile));
 | 
	
		
			
				|  |  | +                                                                    if (File.Exists(opts.WarcFile))
 | 
	
		
			
				|  |  | +                                                                        return ReadKeyFromFile(opts.WarcFile);
 | 
	
		
			
				|  |  | +                                                                    throw new FileNotFoundException("No key found for the requested ARC",
 | 
	
		
			
				|  |  | +                                                                                                    requestedFile);
 | 
	
		
			
				|  |  | +                                                                });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                            using (var output =
 | 
	
		
			
				|  |  | +                                    File.Create(Path.Combine(Path.GetDirectoryName(opts.Output), Path.GetFileNameWithoutExtension(file.Name))))
 | 
	
		
			
				|  |  | +                                arcStream.CopyTo(output);
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                        catch (FileNotFoundException fe)
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            return Errors.Error(Errors.ErrorCodes.NoCorrectKey, file.Name, fe.FileName);
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    else
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return 0;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 |