2022-10-23 21:28:44 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Wabbajack.CLI.Builder;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
using Wabbajack.Compression.BSA;
|
|
|
|
|
using Wabbajack.DTOs;
|
|
|
|
|
using Wabbajack.DTOs.BSA.FileStates;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
using Wabbajack.DTOs.Directives;
|
|
|
|
|
using Wabbajack.DTOs.JsonConverters;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
using Wabbajack.Installer;
|
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
|
using Wabbajack.Paths.IO;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
using Wabbajack.RateLimiter;
|
|
|
|
|
using Wabbajack.VFS;
|
|
|
|
|
using AbsolutePathExtensions = Wabbajack.Common.AbsolutePathExtensions;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
|
|
|
|
|
namespace Wabbajack.CLI.Verbs;
|
|
|
|
|
|
|
|
|
|
public class VerifyModlistInstall
|
|
|
|
|
{
|
|
|
|
|
private readonly DTOSerializer _dtos;
|
|
|
|
|
private readonly ILogger<VerifyModlistInstall> _logger;
|
|
|
|
|
|
2022-10-25 02:12:14 +00:00
|
|
|
|
public VerifyModlistInstall(ILogger<VerifyModlistInstall> logger, DTOSerializer dtos, IResource<FileHashCache> limiter)
|
2022-10-23 21:28:44 +00:00
|
|
|
|
{
|
2022-10-25 02:12:14 +00:00
|
|
|
|
_limiter = limiter;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
_logger = logger;
|
|
|
|
|
_dtos = dtos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static VerbDefinition Definition = new("verify-modlist-install", "Verify a modlist installed correctly",
|
|
|
|
|
new[]
|
|
|
|
|
{
|
|
|
|
|
new OptionDefinition(typeof(AbsolutePath), "m", "modlistLocation",
|
|
|
|
|
"The .wabbajack file used to install the modlist"),
|
|
|
|
|
new OptionDefinition(typeof(AbsolutePath), "i", "installFolder", "The installation folder of the modlist")
|
|
|
|
|
});
|
|
|
|
|
|
2022-10-25 02:12:14 +00:00
|
|
|
|
private readonly IResource<FileHashCache> _limiter;
|
2022-10-23 21:28:44 +00:00
|
|
|
|
|
|
|
|
|
|
2022-10-25 02:12:14 +00:00
|
|
|
|
public async Task<int> Run(AbsolutePath modlistLocation, AbsolutePath installFolder, CancellationToken token)
|
2022-10-23 21:28:44 +00:00
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Loading modlist {ModList}", modlistLocation);
|
|
|
|
|
var list = await StandardInstaller.LoadFromFile(_dtos, modlistLocation);
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Indexing files");
|
|
|
|
|
var byTo = list.Directives.ToDictionary(d => d.To);
|
2022-10-23 21:28:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Scanning files");
|
2022-10-30 13:16:12 +00:00
|
|
|
|
var errors = await list.Directives.PMapAllBatchedAsync(_limiter, async directive =>
|
2022-10-24 23:28:03 +00:00
|
|
|
|
{
|
2022-10-30 13:16:12 +00:00
|
|
|
|
if (!(directive is CreateBSA || directive.IsDeterministic))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (directive.To.InFolder(Consts.BSACreationDir))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
var dest = directive.To.RelativeTo(installFolder);
|
|
|
|
|
if (!dest.FileExists())
|
2022-10-24 23:28:03 +00:00
|
|
|
|
{
|
2022-10-30 13:16:12 +00:00
|
|
|
|
return new Result
|
|
|
|
|
{
|
|
|
|
|
Path = directive.To,
|
|
|
|
|
Message = $"File does not exist directive {directive.GetType()}"
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
if (Consts.KnownModifiedFiles.Contains(directive.To.FileName))
|
|
|
|
|
return null;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
if (directive is CreateBSA bsa)
|
2022-10-23 21:28:44 +00:00
|
|
|
|
{
|
2022-10-30 13:16:12 +00:00
|
|
|
|
return await VerifyBSA(dest, bsa, byTo, token);
|
|
|
|
|
}
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
if (dest.Size() != directive.Size)
|
|
|
|
|
{
|
|
|
|
|
return new Result
|
|
|
|
|
{
|
|
|
|
|
Path = directive.To,
|
|
|
|
|
Message = $"Sizes do not match got {dest.Size()} expected {directive.Size}"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (directive.Size > (1024 * 1024 * 128))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Hashing {Size} file at {Path}", directive.Size.ToFileSizeString(),
|
|
|
|
|
directive.To);
|
|
|
|
|
}
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
var hash = await AbsolutePathExtensions.Hash(dest, token);
|
|
|
|
|
if (hash != directive.Hash)
|
2022-10-25 02:12:14 +00:00
|
|
|
|
{
|
2022-10-30 13:16:12 +00:00
|
|
|
|
return new Result
|
|
|
|
|
{
|
|
|
|
|
Path = directive.To,
|
|
|
|
|
Message = $"Hashes do not match, got {hash} expected {directive.Hash}"
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-10-25 02:12:14 +00:00
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
return null;
|
2022-10-25 02:12:14 +00:00
|
|
|
|
}).Where(r => r != null)
|
|
|
|
|
.ToList();
|
2022-10-23 21:28:44 +00:00
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Found {Count} errors", errors.Count);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var error in errors)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("{File} | {Message}", error.Path, error.Message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-25 02:12:14 +00:00
|
|
|
|
private async Task<Result?> VerifyBSA(AbsolutePath dest, CreateBSA bsa, Dictionary<RelativePath, Directive> byTo, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Verifying Created BSA {To}", bsa.To);
|
|
|
|
|
var archive = await BSADispatch.Open(dest);
|
|
|
|
|
var filesIndexed = archive.Files.ToDictionary(d => d.Path);
|
|
|
|
|
|
|
|
|
|
if (dest.Extension == Ext.Bsa && dest.Size() >= 1024L * 1024 * 1024 * 2)
|
|
|
|
|
{
|
|
|
|
|
return new Result()
|
|
|
|
|
{
|
|
|
|
|
Path = bsa.To,
|
|
|
|
|
Message = $"BSA is over 2GB in size, this will cause crashes : {bsa.To}"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var file in bsa.FileStates)
|
|
|
|
|
{
|
|
|
|
|
if (file is BA2DX10File) continue;
|
|
|
|
|
var state = filesIndexed[file.Path];
|
|
|
|
|
var sf = await state.GetStreamFactory(token);
|
|
|
|
|
await using var stream = await sf.GetStream();
|
|
|
|
|
var hash = await stream.Hash(token);
|
|
|
|
|
|
|
|
|
|
var astate = bsa.FileStates.First(f => f.Path == state.Path);
|
|
|
|
|
var srcDirective = byTo[Consts.BSACreationDir.Combine(bsa.TempID, astate.Path)];
|
|
|
|
|
|
2022-10-30 13:16:12 +00:00
|
|
|
|
if (!srcDirective.IsDeterministic)
|
|
|
|
|
continue;
|
|
|
|
|
|
2022-10-25 02:12:14 +00:00
|
|
|
|
if (srcDirective.Hash != hash)
|
|
|
|
|
{
|
|
|
|
|
return new Result
|
|
|
|
|
{
|
|
|
|
|
Path = bsa.To,
|
|
|
|
|
Message =
|
|
|
|
|
$"BSA {bsa.To} contents do not match at {file.Path} got {hash} expected {srcDirective.Hash}"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-23 21:28:44 +00:00
|
|
|
|
public class Result
|
|
|
|
|
{
|
|
|
|
|
public RelativePath Path { get; set; }
|
|
|
|
|
public string Message { get; set; }
|
|
|
|
|
}
|
|
|
|
|
}
|