public static void DecodeVerb(DecodeOptions opt)
        {
            var privDir = new DirectoryInfo(Path.Combine(opt.CacheDirectory));

            if (!privDir.Exists)
            {
                Console.WriteLine("Cache directory not found?");
                return;
            }

            var workDir = string.IsNullOrWhiteSpace(opt.WorkingDirectory) ? Environment.CurrentDirectory : opt.WorkingDirectory;
            var dbDir   = new DirectoryInfo(Path.Combine(workDir, "db"));

            Console.WriteLine("Decrypting database");
            DecryptDb(new DirectoryInfo(Path.Combine(privDir.FullName, "db")), dbDir);

            Console.WriteLine("Reading entries from database");
            var entries = ReadDatabaseEntries(dbDir.FullName, out var errorCount);

            Console.WriteLine("Found {0} entries", entries.Count);
            Console.WriteLine("Errors reading {0} entries", errorCount);

            var entryLookup = entries.ToLookup(e => e.Filename);
            var files       = new DirectoryInfo(privDir.FullName).GetFiles();
            var filesLookup = files.ToLookup(f => f.Name);

            var entriesWithFiles = entries.Select(e => new { entry = e, file = filesLookup[e.Filename].FirstOrDefault() }).ToList();

            Console.WriteLine("Skipping {0} entries", entriesWithFiles.Count(e => e.file == null));
            Console.WriteLine("Skipping {0} files", files.Count(e => !entryLookup[e.Name].Any()));
            Console.WriteLine("Decrypting {0} files", entriesWithFiles.Count(e => e.file != null));

            foreach (var g in entriesWithFiles.Where(e => e.file != null).GroupBy(e => Tuple.Create(e.entry.ResourceName, e.entry.OriginalFilename)))
            {
                foreach (var entryAndFile in g.OrderByDescending(e => e.file.LastWriteTimeUtc).ToList())
                {
                    var entry  = entryAndFile.entry;
                    var input  = File.ReadAllBytes(filesLookup[entry.Filename].First().FullName);
                    var output = Decrypt(DecryptStatic(input), entry.Key, entry.IV);

                    var timeStamp = entryAndFile.file.LastWriteTimeUtc;

                    var outDirStr = Regex.Replace(opt.OutputDirectory, "(%\\w)", res =>
                    {
                        switch (res.Groups[1].Value)
                        {
                        case "%d":
                            return(timeStamp.Day.ToString());

                        case "%m":
                            return(timeStamp.Month.ToString());

                        case "%y":
                            return(timeStamp.Year.ToString());

                        case "%h":
                            return(entry.Hash);

                        case "%n":
                            return(entry.ResourceName);

                        case "%s":
                            var uri = new Uri(entry.From);
                            return(uri.Host + "_" + uri.Port);
                        }
                        return(res.Groups[1].Value);
                    });

                    var outDir = new DirectoryInfo(outDirStr);
                    if (!outDir.Exists)
                    {
                        outDir.Create();
                    }
                    Directory.SetLastWriteTimeUtc(outDir.FullName, timeStamp);

                    if (Path.GetExtension(entry.OriginalFilename) == ".rpf")
                    {
                        var reader = new Rpf2Reader(new MemoryStream(output));
                        reader.Open();
                        foreach (var rpfEntry in reader.ReadEntries())
                        {
                            var outFn = new FileInfo(Path.Combine(outDir.FullName, entry.OriginalFilename, rpfEntry.FullName));
                            if (!outFn.Directory.Exists)
                            {
                                outFn.Directory.Create();
                            }
                            File.WriteAllBytes(outFn.FullName, rpfEntry.Data);
                            File.SetLastWriteTimeUtc(outFn.FullName, timeStamp);
                        }
                    }
                    else
                    {
                        var outFn = Path.Combine(outDir.FullName, entry.OriginalFilename);
                        File.WriteAllBytes(outFn, output);
                        File.SetLastWriteTimeUtc(outFn, timeStamp);
                    }

                    if (!opt.Duplicates)
                    {
                        break;
                    }
                }
            }
            Console.WriteLine("Finished");
        }
        public static void EncodeVerb(EncodeOptions opt)
        {
            var privDir = new DirectoryInfo(Path.Combine(opt.CacheDirectory));

            if (!privDir.Exists)
            {
                Console.WriteLine("Cache directory not found?");
                return;
            }

            var workDir = string.IsNullOrWhiteSpace(opt.WorkingDirectory) ? Environment.CurrentDirectory : opt.WorkingDirectory;
            var dbDir   = new DirectoryInfo(Path.Combine(workDir, "db"));

            Console.WriteLine("Reading entries from database");
            var dbEntries = ReadDatabaseEntries(dbDir.FullName, out var errorCount);

            Console.WriteLine("Found {0} entries", dbEntries.Count);
            Console.WriteLine("Errors reading {0} entries", errorCount);

            var entryLookup = dbEntries.ToLookup(e => e.Filename);
            var files       = new DirectoryInfo(privDir.FullName).GetFiles();
            var filesLookup = files.ToLookup(f => f.Name);

            var entriesWithFiles = dbEntries.Select(e => new { entry = e, file = filesLookup[e.Filename].FirstOrDefault() }).ToList();

            Console.WriteLine("Skipping {0} entries", entriesWithFiles.Count(e => e.file == null));
            Console.WriteLine("Skipping {0} files", files.Count(e => !entryLookup[e.Name].Any()));
            Console.WriteLine("Checking {0} files", entriesWithFiles.Count(e => e.file != null));

            foreach (var g in entriesWithFiles.Where(e => e.file != null).GroupBy(e => Tuple.Create(e.entry.ResourceName, e.entry.OriginalFilename)))
            {
                var entryAndFile = g.OrderByDescending(e => e.file.LastWriteTimeUtc).First();


                var entry    = entryAndFile.entry;
                var outputFn = filesLookup[entry.Filename].First().FullName;
                var input    = File.ReadAllBytes(outputFn);
                var output   = Decrypt(DecryptStatic(input, out var origIv), entry.Key, entry.IV);


                var outDirStr = Regex.Replace(opt.OutputDirectory, "(%\\w)", res =>
                {
                    switch (res.Groups[1].Value)
                    {
                    case "%h":
                        return(entry.Hash);

                    case "%n":
                        return(entry.ResourceName);

                    case "%s":
                        var uri = new Uri(entry.From);
                        return(uri.Host + "_" + uri.Port);
                    }
                    return(res.Groups[1].Value);
                });

                var outDir = new DirectoryInfo(outDirStr);
                if (!outDir.Exists)
                {
                    Console.WriteLine("Skipping: {0}/{1}", entry.ResourceName, entry.OriginalFilename);
                    continue;
                }



                if (Path.GetExtension(entry.OriginalFilename) == ".rpf")
                {
                    var reader = new Rpf2Reader(new MemoryStream(output));
                    reader.Open();
                    var entries = reader.ReadEntries();
                    var rpfDir  = new DirectoryInfo(Path.Combine(outDir.FullName, entry.OriginalFilename));
                    if (!rpfDir.Exists)
                    {
                        Console.WriteLine("Skipping rpf: {0}", rpfDir.FullName);
                        continue;
                    }
                    var rpfFiles = rpfDir.GetFiles("*", SearchOption.AllDirectories);

                    var lookup   = entries.ToLookup(e => Path.GetFullPath(Path.Combine(rpfDir.FullName, e.FullName)));
                    var matches  = entries.Join(rpfFiles, e => Path.GetFullPath(Path.Combine(rpfDir.FullName, e.FullName)), f => f.FullName, (e, f) => new { entry = e, file = f }).ToList();
                    var newFiles = rpfFiles.Where(f => !lookup[f.FullName].Any()).ToList();
                    var differ   = rpfFiles.Length != matches.Count;
                    foreach (var m in matches)
                    {
                        var oldHash = Hash(File.ReadAllBytes(m.file.FullName));
                        var newHash = Hash(m.entry.Data);
                        if (oldHash != newHash)
                        {
                            Console.WriteLine("Rpf file change: {0}", m.entry.FullName);
                            differ = true;
                        }
                    }
                    foreach (var nf in newFiles)
                    {
                        Console.WriteLine("Rpf file new: {0}", nf.FullName);
                    }

                    if (differ)
                    {
                        Console.WriteLine("Overwriting {0}", entry.ResourceName);

                        if (!opt.DryRun)
                        {
                            using (var ms = new MemoryStream())
                            {
                                var rpfWr = new Rpf2Writer(ms);
                                rpfWr.Write(rpfDir);
                                File.WriteAllBytes(outputFn, EncryptStatic(Encrypt(ms.ToArray(), entry.Key, entry.IV), origIv));
                            }
                        }
                    }
                }
                else
                {
                    var inFn = Path.Combine(outDir.FullName, entry.OriginalFilename);
                    if (!File.Exists(inFn))
                    {
                        Console.WriteLine("Skipping: {0}", inFn);
                        continue;
                    }
                    var inData  = File.ReadAllBytes(inFn);
                    var inHash  = Hash(inData);
                    var oldHash = Hash(output);

                    if (inHash != oldHash)
                    {
                        Console.WriteLine("Overwriting {0} with {1}", entry.Filename, inFn);
                        if (!opt.DryRun)
                        {
                            File.WriteAllBytes(outputFn, EncryptStatic(Encrypt(inData, entry.Key, entry.IV), origIv));
                        }
                    }
                }
            }
            Console.WriteLine("Finished" + (opt.DryRun ? " (Dry Run. Nothing was saved)" : ""));
        }