public static async Task <IndexedZiPatchInstaller> VerifyFromZiPatchIndex(IndexedZiPatchIndex patchIndex, string gameRootPath, CancellationToken?cancellationToken = null)
        {
            using var verifier = new IndexedZiPatchInstaller(patchIndex)
                  {
                      ProgressReportInterval = 1000
                  };

            var remainingErrorMessagesToShow = 8;

            void OnVerifyProgressCallback(int index, long progress, long max) => Log.Information("[{0}/{1}] Checking file {2}... {3:0.00}/{4:0.00}MB ({5:00.00}%)", index + 1, patchIndex.Length, patchIndex[Math.Min(index, patchIndex.Length - 1)].RelativePath, progress / 1048576.0, max / 1048576.0, 100.0 * progress / max);;
            void OnCorruptionFoundCallback(IndexedZiPatchPartLocator part, IndexedZiPatchPartLocator.VerifyDataResult result)
            {
                switch (result)
                {
                case IndexedZiPatchPartLocator.VerifyDataResult.FailNotEnoughData:
                    if (remainingErrorMessagesToShow > 0)
                    {
                        Log.Error("{0}:{1}:{2}: Premature EOF detected", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, patchIndex[part.TargetIndex].FileSize);
                        remainingErrorMessagesToShow = 0;
                    }
                    break;

                case IndexedZiPatchPartLocator.VerifyDataResult.FailBadData:
                    if (remainingErrorMessagesToShow > 0)
                    {
                        if (--remainingErrorMessagesToShow == 0)
                        {
                            Log.Warning("{0}:{1}:{2}: Corrupt data; suppressing further corruption warnings for this file.", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, part.TargetEnd);
                        }
                        else
                        {
                            Log.Warning("{0}:{1}:{2}: Corrupt data", patchIndex[part.TargetIndex].RelativePath, part.TargetOffset, part.TargetEnd);
                        }
                    }
                    break;
                }
            };

            verifier.OnVerifyProgress  += OnVerifyProgressCallback;
            verifier.OnCorruptionFound += OnCorruptionFoundCallback;

            try
            {
                verifier.SetTargetStreamsFromPathReadOnly(gameRootPath);
                await verifier.VerifyFiles(false, 8, cancellationToken);
            }
            finally
            {
                verifier.OnVerifyProgress  -= OnVerifyProgressCallback;
                verifier.OnCorruptionFound -= OnCorruptionFoundCallback;
            }

            return(verifier);
        }
        public static async Task RepairFromPatchFileIndexFromFile(IndexedZiPatchIndex patchIndex, string gameRootPath, string patchFileRootDir, int concurrentCount, CancellationToken?cancellationToken = null)
        {
            using var verifier = await VerifyFromZiPatchIndex(patchIndex, gameRootPath, cancellationToken);

            verifier.SetTargetStreamsFromPathReadWriteForMissingFiles(gameRootPath);
            for (var i = 0; i < patchIndex.Sources.Count; i++)
            {
                verifier.QueueInstall(i, new FileInfo(Path.Combine(patchFileRootDir, patchIndex.Sources[i])));
            }
            await verifier.Install(concurrentCount, cancellationToken);
        }
        public static async Task RepairFromPatchFileIndexFromUri(IndexedZiPatchIndex patchIndex, string gameRootPath, string baseUri, int concurrentCount, CancellationToken?cancellationToken = null)
        {
            using var verifier = await VerifyFromZiPatchIndex(patchIndex, gameRootPath, cancellationToken);

            verifier.SetTargetStreamsFromPathReadWriteForMissingFiles(gameRootPath);
            for (var i = 0; i < patchIndex.Sources.Count; i++)
            {
                verifier.QueueInstall(i, baseUri + patchIndex.Sources[i], null, concurrentCount);
            }

            void OnInstallProgressCallback(int index, long progress, long max, IndexedZiPatchInstaller.InstallTaskState state) => Log.Information("[{0}/{1}] {2} {3}... {4:0.00}/{5:0.00}MB ({6:00.00}%)", index, patchIndex.Sources.Count, state, patchIndex.Sources[Math.Min(index, patchIndex.Sources.Count - 1)], progress / 1048576.0, max / 1048576.0, 100.0 * progress / max);

            verifier.OnInstallProgress += OnInstallProgressCallback;
            try
            {
                await verifier.Install(concurrentCount, cancellationToken);

                verifier.WriteVersionFiles(gameRootPath);
            }
            finally
            {
                verifier.OnInstallProgress -= OnInstallProgressCallback;
            }
        }
        public static async Task <IndexedZiPatchIndex> CreateZiPatchIndices(int expacVersion, IList <string> patchFilePaths, CancellationToken?cancellationToken = null)
        {
            var sources    = new List <Stream>();
            var patchFiles = new List <ZiPatchFile>();
            var patchIndex = new IndexedZiPatchIndex(expacVersion);

            try
            {
                var firstPatchFileIndex = patchFilePaths.Count - 1;
                while (firstPatchFileIndex > 0)
                {
                    if (File.Exists(patchFilePaths[firstPatchFileIndex] + ".index"))
                    {
                        break;
                    }
                    firstPatchFileIndex--;
                }
                for (var i = 0; i < patchFilePaths.Count; ++i)
                {
                    if (cancellationToken.HasValue)
                    {
                        cancellationToken.Value.ThrowIfCancellationRequested();
                    }

                    var patchFilePath = patchFilePaths[i];
                    sources.Add(new FileStream(patchFilePath, FileMode.Open, FileAccess.Read));
                    patchFiles.Add(new ZiPatchFile(sources[sources.Count - 1]));

                    if (i < firstPatchFileIndex)
                    {
                        continue;
                    }

                    if (File.Exists(patchFilePath + ".index"))
                    {
                        Log.Information("Reading patch index file {0}...", patchFilePath);
                        patchIndex = new(new BinaryReader(new DeflateStream(new FileStream(patchFilePath + ".index", FileMode.Open, FileAccess.Read), CompressionMode.Decompress)));
                        continue;
                    }

                    Log.Information("Indexing patch file {0}...", patchFilePath);
                    await patchIndex.ApplyZiPatch(Path.GetFileName(patchFilePath), patchFiles[patchFiles.Count - 1], cancellationToken);

                    Log.Information("Calculating CRC32 for files resulted from patch file {0}...", patchFilePath);
                    await patchIndex.CalculateCrc32(sources, cancellationToken);

                    using (var writer = new BinaryWriter(new DeflateStream(new FileStream(patchFilePath + ".index.tmp", FileMode.Create), CompressionLevel.Optimal)))
                        patchIndex.WriteTo(writer);

                    File.Move(patchFilePath + ".index.tmp", patchFilePath + ".index");
                }

                return(patchIndex);
            }
            finally
            {
                foreach (var source in sources)
                {
                    source.Dispose();
                }
            }
        }