WIP, converted Hashes to a Hash struct

This commit is contained in:
Timothy Baldridge 2020-03-22 09:50:53 -06:00
parent e4ecaa882c
commit 3b895f4dbb
42 changed files with 314 additions and 649 deletions

View File

@ -38,7 +38,7 @@ namespace Wabbajack.BuildServer.Controllers
[Route("{xxHashAsBase64}/meta.ini")] [Route("{xxHashAsBase64}/meta.ini")]
public async Task<IActionResult> GetFileMeta(string xxHashAsBase64) public async Task<IActionResult> GetFileMeta(string xxHashAsBase64)
{ {
var id = xxHashAsBase64.FromHex().ToBase64(); var id = Hash.FromHex(xxHashAsBase64);
var state = await Db.DownloadStates.AsQueryable() var state = await Db.DownloadStates.AsQueryable()
.Where(d => d.Hash == id && d.IsValid) .Where(d => d.Hash == id && d.IsValid)
.OrderByDescending(d => d.LastValidationTime) .OrderByDescending(d => d.LastValidationTime)
@ -51,48 +51,6 @@ namespace Wabbajack.BuildServer.Controllers
return Ok(string.Join("\r\n", state.FirstOrDefault().State.GetMetaIni())); return Ok(string.Join("\r\n", state.FirstOrDefault().State.GetMetaIni()));
} }
[Authorize]
[HttpDelete]
[Route("/indexed_files/nexus/{Game}/mod/{ModId}")]
public async Task<IActionResult> PurgeBySHA256(string Game, string ModId)
{
var files = await Db.DownloadStates.AsQueryable().Where(d => d.State is NexusDownloader.State &&
((NexusDownloader.State)d.State).GameName == Game &&
((NexusDownloader.State)d.State).ModID == ModId)
.ToListAsync();
async Task DeleteParentsOf(HashSet<string> acc, string hash)
{
var parents = await Db.IndexedFiles.AsQueryable().Where(f => f.Children.Any(c => c.Hash == hash))
.ToListAsync();
foreach (var parent in parents)
await DeleteThisAndAllChildren(acc, parent.Hash);
}
async Task DeleteThisAndAllChildren(HashSet<string> acc, string hash)
{
acc.Add(hash);
var children = await Db.IndexedFiles.AsQueryable().Where(f => f.Hash == hash).FirstOrDefaultAsync();
if (children == null) return;
foreach (var child in children.Children)
{
await DeleteThisAndAllChildren(acc, child.Hash);
}
}
var acc = new HashSet<string>();
foreach (var file in files)
await DeleteThisAndAllChildren(acc, file.Hash);
var acclst = acc.ToList();
await Db.DownloadStates.DeleteManyAsync(d => acc.Contains(d.Hash));
await Db.IndexedFiles.DeleteManyAsync(d => acc.Contains(d.Hash));
return Ok(acc.ToList());
}
[HttpPost] [HttpPost]
[Route("notify")] [Route("notify")]
public async Task<IActionResult> Notify() public async Task<IActionResult> Notify()

View File

@ -43,7 +43,7 @@ namespace Wabbajack.BuildServer.Controllers
{ {
var lists = await Db.ModListStatus.AsQueryable().ToListAsync(); var lists = await Db.ModListStatus.AsQueryable().ToListAsync();
var archives = lists.SelectMany(list => list.DetailedStatus.Archives) var archives = lists.SelectMany(list => list.DetailedStatus.Archives)
.Select(a => a.Archive.Hash.FromBase64().ToHex()) .Select(a => a.Archive.Hash.ToHex())
.ToHashSet(); .ToHashSet();
var toDelete = new List<string>(); var toDelete = new List<string>();
@ -89,9 +89,9 @@ namespace Wabbajack.BuildServer.Controllers
[Route("/alternative/{xxHash}")] [Route("/alternative/{xxHash}")]
public async Task<IActionResult> GetAlternative(string xxHash) public async Task<IActionResult> GetAlternative(string xxHash)
{ {
var startingHash = xxHash.FromHex().ToBase64(); var startingHash = Hash.FromHex(xxHash);
Utils.Log($"Alternative requested for {startingHash}"); Utils.Log($"Alternative requested for {startingHash}");
await Metric("requested_upgrade", startingHash); await Metric("requested_upgrade", startingHash.ToString());
var state = await Db.DownloadStates.AsQueryable() var state = await Db.DownloadStates.AsQueryable()
.Where(s => s.Hash == startingHash) .Where(s => s.Hash == startingHash)
@ -110,7 +110,7 @@ namespace Wabbajack.BuildServer.Controllers
if (mod_files.SelectMany(f => f.Data.files) if (mod_files.SelectMany(f => f.Data.files)
.Any(f => f.category_name != null && f.file_id.ToString() == nexusState.FileID)) .Any(f => f.category_name != null && f.file_id.ToString() == nexusState.FileID))
{ {
await Metric("not_required_upgrade", startingHash); await Metric("not_required_upgrade", startingHash.ToString());
return BadRequest("Upgrade Not Required"); return BadRequest("Upgrade Not Required");
} }
@ -122,7 +122,7 @@ namespace Wabbajack.BuildServer.Controllers
} }
Utils.Log($"Found {newArchive.State.PrimaryKeyString} {newArchive.Name} as an alternative to {startingHash}"); Utils.Log($"Found {newArchive.State.PrimaryKeyString} {newArchive.Name} as an alternative to {startingHash}");
if (newArchive.Hash == null) if (newArchive.Hash == Hash.Empty)
{ {
Db.Jobs.InsertOne(new Job Db.Jobs.InsertOne(new Job
{ {
@ -160,7 +160,7 @@ namespace Wabbajack.BuildServer.Controllers
return Ok(newArchive.ToJSON()); return Ok(newArchive.ToJSON());
} }
private async Task<Archive> FindAlternatives(NexusDownloader.State state, string srcHash) private async Task<Archive> FindAlternatives(NexusDownloader.State state, Hash srcHash)
{ {
var origSize = AlphaFile.GetSize(_settings.PathForArchive(srcHash)); var origSize = AlphaFile.GetSize(_settings.PathForArchive(srcHash));
var api = await NexusApiClient.Get(Request.Headers["apikey"].FirstOrDefault()); var api = await NexusApiClient.Get(Request.Headers["apikey"].FirstOrDefault());

View File

@ -121,7 +121,7 @@ namespace Wabbajack.BuildServer.Controllers
[Route("upload_file/{Key}/finish/{xxHashAsHex}")] [Route("upload_file/{Key}/finish/{xxHashAsHex}")]
public async Task<IActionResult> UploadFileFinish(string Key, string xxHashAsHex) public async Task<IActionResult> UploadFileFinish(string Key, string xxHashAsHex)
{ {
var expectedHash = xxHashAsHex.FromHex().ToBase64(); var expectedHash = Hash.FromHex(xxHashAsHex);
var user = User.FindFirstValue(ClaimTypes.Name); var user = User.FindFirstValue(ClaimTypes.Name);
if (!Key.All(a => HexChars.Contains(a))) if (!Key.All(a => HexChars.Contains(a)))
return BadRequest("NOT A VALID FILENAME"); return BadRequest("NOT A VALID FILENAME");

View File

@ -47,13 +47,13 @@ namespace Wabbajack.BuildServer
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options); return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
} }
private static ConcurrentDictionary<string, string> PathForArchiveHash = new ConcurrentDictionary<string, string>(); private static readonly ConcurrentDictionary<Hash, string> PathForArchiveHash = new ConcurrentDictionary<Hash, string>();
public static string PathForArchive(this AppSettings settings, string hash) public static string PathForArchive(this AppSettings settings, Hash hash)
{ {
if (PathForArchiveHash.TryGetValue(hash, out string result)) if (PathForArchiveHash.TryGetValue(hash, out string result))
return result; return result;
var hexHash = hash.FromBase64().ToHex(); var hexHash = hash.ToHex();
var ends = "_" + hexHash + "_"; var ends = "_" + hexHash + "_";
var file = Directory.EnumerateFiles(settings.ArchiveDir, DirectoryEnumerationOptions.Files, var file = Directory.EnumerateFiles(settings.ArchiveDir, DirectoryEnumerationOptions.Files,

View File

@ -1,9 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.Downloaders;
namespace Wabbajack.BuildServer.Models namespace Wabbajack.BuildServer.Models
@ -12,7 +9,7 @@ namespace Wabbajack.BuildServer.Models
{ {
[BsonId] [BsonId]
public string Key { get; set; } public string Key { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
public AbstractDownloadState State { get; set; } public AbstractDownloadState State { get; set; }

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Wabbajack.Common;
using Wabbajack.VirtualFileSystem; using Wabbajack.VirtualFileSystem;
namespace Wabbajack.BuildServer.Models namespace Wabbajack.BuildServer.Models
@ -11,7 +12,7 @@ namespace Wabbajack.BuildServer.Models
public class IndexedFile public class IndexedFile
{ {
[BsonId] [BsonId]
public string Hash { get; set; } public Hash Hash { get; set; }
public string SHA256 { get; set; } public string SHA256 { get; set; }
public string SHA1 { get; set; } public string SHA1 { get; set; }
public string MD5 { get; set; } public string MD5 { get; set; }
@ -25,6 +26,6 @@ namespace Wabbajack.BuildServer.Models
{ {
public string Name; public string Name;
public string Extension; public string Extension;
public string Hash; public Hash Hash;
} }
} }

View File

@ -56,7 +56,7 @@ namespace Wabbajack.BuildServer.Models.Jobs
}); });
var to_path = Path.Combine(settings.ArchiveDir, var to_path = Path.Combine(settings.ArchiveDir,
$"{Path.GetFileName(fileName)}_{archive.Hash.FromBase64().ToHex()}_{Path.GetExtension(fileName)}"); $"{Path.GetFileName(fileName)}_{archive.Hash.ToHex()}_{Path.GetExtension(fileName)}");
if (File.Exists(to_path)) if (File.Exists(to_path))
File.Delete(downloadDest); File.Delete(downloadDest);
else else

View File

@ -27,7 +27,7 @@ namespace Wabbajack.BuildServer.Models.Jobs
using (var queue = new WorkQueue()) using (var queue = new WorkQueue())
{ {
var whitelists = new ValidateModlist(queue); var whitelists = new ValidateModlist();
await whitelists.LoadListsFromGithub(); await whitelists.LoadListsFromGithub();
foreach (var list in modlists) foreach (var list in modlists)

View File

@ -18,7 +18,7 @@ namespace Wabbajack.BuildServer.Models
public class PatchArchive : AJobPayload public class PatchArchive : AJobPayload
{ {
public override string Description => "Create a archive update patch"; public override string Description => "Create a archive update patch";
public string Src { get; set; } public Hash Src { get; set; }
public string DestPK { get; set; } public string DestPK { get; set; }
public override async Task<JobResult> Execute(DBContext db, SqlService sql, AppSettings settings) public override async Task<JobResult> Execute(DBContext db, SqlService sql, AppSettings settings)
{ {
@ -56,7 +56,7 @@ namespace Wabbajack.BuildServer.Models
await client.ConnectAsync(); await client.ConnectAsync();
try try
{ {
await client.UploadAsync(fs, $"updates/{Src.FromBase64().ToHex()}_{destHash.FromBase64().ToHex()}", progress: new UploadToCDN.Progress(cdnPath)); await client.UploadAsync(fs, $"updates/{Src.ToHex()}_{destHash.ToHex()}", progress: new UploadToCDN.Progress(cdnPath));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -72,9 +72,9 @@ namespace Wabbajack.BuildServer.Models
} }
public static string CdnPath(string srcHash, string destHash) public static string CdnPath(Hash srcHash, Hash destHash)
{ {
return $"updates/{srcHash.FromBase64().ToHex()}_{destHash.FromBase64().ToHex()}"; return $"updates/{srcHash.ToHex()}_{destHash.ToHex()}";
} }
} }
} }

View File

@ -49,10 +49,9 @@ namespace Wabbajack.BuildServer.Model.Models
private static void IngestFile(VirtualFile root, ICollection<IndexedFile> files, ICollection<ArchiveContent> contents) private static void IngestFile(VirtualFile root, ICollection<IndexedFile> files, ICollection<ArchiveContent> contents)
{ {
var hash = BitConverter.ToInt64(root.Hash.FromBase64());
files.Add(new IndexedFile files.Add(new IndexedFile
{ {
Hash = hash, Hash = (long)root.Hash,
Sha256 = root.ExtendedHashes.SHA256.FromHex(), Sha256 = root.ExtendedHashes.SHA256.FromHex(),
Sha1 = root.ExtendedHashes.SHA1.FromHex(), Sha1 = root.ExtendedHashes.SHA1.FromHex(),
Md5 = root.ExtendedHashes.MD5.FromHex(), Md5 = root.ExtendedHashes.MD5.FromHex(),
@ -66,22 +65,21 @@ namespace Wabbajack.BuildServer.Model.Models
{ {
IngestFile(child, files, contents); IngestFile(child, files, contents);
var child_hash = BitConverter.ToInt64(child.Hash.FromBase64());
contents.Add(new ArchiveContent contents.Add(new ArchiveContent
{ {
Parent = hash, Parent = (long)root.Hash,
Child = child_hash, Child = (long)child.Hash,
Path = child.Name Path = child.Name
}); });
} }
} }
public async Task<bool> HaveIndexdFile(string hash) public async Task<bool> HaveIndexdFile(Hash hash)
{ {
await using var conn = await Open(); await using var conn = await Open();
var row = await conn.QueryAsync(@"SELECT * FROM IndexedFile WHERE Hash = @Hash", var row = await conn.QueryAsync(@"SELECT * FROM IndexedFile WHERE Hash = @Hash",
new {Hash = BitConverter.ToInt64(hash.FromBase64())}); new {Hash = (long)hash});
return row.Any(); return row.Any();
} }
@ -123,7 +121,7 @@ namespace Wabbajack.BuildServer.Model.Models
return children.Select(f => new IndexedVirtualFile return children.Select(f => new IndexedVirtualFile
{ {
Name = f.Path, Name = f.Path,
Hash = BitConverter.GetBytes(f.Hash).ToBase64(), Hash = Hash.FromLong(f.Hash),
Size = f.Size, Size = f.Size,
Children = Build(f.Hash) Children = Build(f.Hash)
}).ToList(); }).ToList();

View File

@ -13,7 +13,7 @@ namespace Wabbajack.BuildServer.Models
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public long Size { get; set; } public long Size { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
public string Uploader { get; set; } public string Uploader { get; set; }
public DateTime UploadDate { get; set; } = DateTime.UtcNow; public DateTime UploadDate { get; set; } = DateTime.UtcNow;

View File

@ -57,7 +57,7 @@ namespace Wabbajack.CLI.Verbs
try try
{ {
ValidateModlist.RunValidation(queue, modlist).RunSynchronously(); ValidateModlist.RunValidation(modlist).RunSynchronously();
} }
catch (Exception e) catch (Exception e)
{ {

198
Wabbajack.Common/Hash.cs Normal file
View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Data.HashFunction.xxHash;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using File = Alphaleonis.Win32.Filesystem.File;
using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Common
{
/// <summary>
/// Struct representing a xxHash64 value. It's a struct with a ulong in it, but wrapped so we don't confuse
/// it with other longs in the system.
/// </summary>
public struct Hash
{
private readonly ulong _code;
public Hash(ulong code = 0)
{
_code = code;
}
public override string ToString()
{
return BitConverter.GetBytes(_code).ToBase64();
}
public override bool Equals(object? obj)
{
if (obj is Hash h)
return h._code == _code;
return false;
}
public override int GetHashCode()
{
return (int)(_code >> 32) ^ (int)_code;
}
public static bool operator ==(Hash a, Hash b)
{
return a._code == b._code;
}
public static bool operator !=(Hash a, Hash b)
{
return !(a == b);
}
public static explicit operator ulong(Hash a)
{
return a._code;
}
public static explicit operator long(Hash a)
{
return BitConverter.ToInt64(BitConverter.GetBytes(a._code));
}
public string ToHex()
{
return BitConverter.GetBytes(_code).ToHex();
}
public string ToBase64()
{
return BitConverter.GetBytes(_code).ToBase64();
}
public static Hash FromBase64(string hash)
{
return new Hash(BitConverter.ToUInt64(hash.FromBase64()));
}
public static Hash Empty = new Hash();
public static Hash FromLong(in long argHash)
{
return new Hash(BitConverter.ToUInt64(BitConverter.GetBytes(argHash)));
}
public static Hash FromHex(string xxHashAsHex)
{
return new Hash(BitConverter.ToUInt64(xxHashAsHex.FromHex()));
}
}
public static partial class Utils
{
public static Hash ReadHash(this BinaryReader br)
{
return new Hash(br.ReadUInt64());
}
public static void Write(this BinaryWriter bw, Hash hash)
{
bw.Write((ulong)hash);
}
public static string StringSha256Hex(this string s)
{
var sha = new SHA256Managed();
using (var o = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write))
{
using var i = new MemoryStream(Encoding.UTF8.GetBytes(s));
i.CopyTo(o);
}
return sha.Hash.ToHex();
}
public static Hash FileHash(this string file, bool nullOnIoError = false)
{
try
{
using var fs = File.OpenRead(file);
var config = new xxHashConfig {HashSizeInBits = 64};
using var f = new StatusFileStream(fs, $"Hashing {Path.GetFileName(file)}");
return new Hash(BitConverter.ToUInt64(xxHashFactory.Instance.Create(config).ComputeHash(f).Hash));
}
catch (IOException)
{
if (nullOnIoError) return Hash.Empty;
throw;
}
}
public static Hash FileHashCached(this string file, bool nullOnIoError = false)
{
if (TryGetHashCache(file, out var foundHash)) return foundHash;
var hash = file.FileHash(nullOnIoError);
if (hash != Hash.Empty)
WriteHashCache(file, hash);
return hash;
}
public static bool TryGetHashCache(string file, out Hash hash)
{
var hashFile = file + Consts.HashFileExtension;
hash = Hash.Empty;
if (!File.Exists(hashFile)) return false;
if (File.GetSize(hashFile) != 20) return false;
using var fs = File.OpenRead(hashFile);
using var br = new BinaryReader(fs);
var version = br.ReadUInt32();
if (version != HashCacheVersion) return false;
var lastModified = br.ReadUInt64();
if (lastModified != File.GetLastWriteTimeUtc(file).AsUnixTime()) return false;
hash = new Hash(br.ReadUInt64());
return true;
}
private const uint HashCacheVersion = 0x01;
private static void WriteHashCache(string file, Hash hash)
{
using var fs = File.Create(file + Consts.HashFileExtension);
using var bw = new BinaryWriter(fs);
bw.Write(HashCacheVersion);
var lastModified = File.GetLastWriteTimeUtc(file).AsUnixTime();
bw.Write(lastModified);
bw.Write((ulong)hash);
}
public static async Task<Hash> FileHashCachedAsync(this string file, bool nullOnIOError = false)
{
if (TryGetHashCache(file, out var foundHash)) return foundHash;
var hash = await file.FileHashAsync(nullOnIOError);
if (hash != Hash.Empty)
WriteHashCache(file, hash);
return hash;
}
public static async Task<Hash> FileHashAsync(this string file, bool nullOnIOError = false)
{
try
{
await using var fs = File.OpenRead(file);
var config = new xxHashConfig {HashSizeInBits = 64};
var value = await xxHashFactory.Instance.Create(config).ComputeHashAsync(fs);
return new Hash(BitConverter.ToUInt64(value.Hash));
}
catch (IOException)
{
if (nullOnIOError) return Hash.Empty;
throw;
}
}
}
}

View File

@ -1,9 +0,0 @@
using System.IO;
namespace Wabbajack.Common.Serialization
{
public class Deserializer
{
public BinaryReader Reader { get; }
}
}

View File

@ -1,9 +0,0 @@
namespace Wabbajack.Common.Serialization
{
public interface IHandler
{
public void Write<T>(Serializer serializer, T data);
public T Read<T>(Deserializer deserialiser);
}
}

View File

@ -1,34 +0,0 @@
using System;
namespace Wabbajack.Common.Serialization {
public class UInt32Handler : IHandler {
public void Write<T>(Serializer serializer, UInt32 data)
{
serializer.Writer.Write(data);
}
public T Read<T>(Deserializer deserializer)
{
return deserializer.Reader.ReadUInt32();
}
}
public class Int32Handler : IHandler {
public void Write<T>(Serializer serializer, Int32 data)
{
serializer.Writer.Write(data);
}
public T Read<T>(Deserializer deserializer)
{
return deserializer.Reader.ReadInt32();
}
}
}

View File

@ -1,34 +0,0 @@
<#@ template language="C#" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
namespace Wabbajack.Common.Serialization {
<#
var types = new List<(Type, string)>()
{
(typeof(UInt32), "UInt32"),
(typeof(Int32), "Int32")
};
foreach (var type in types)
{
#>
public class <#=type.Item2#>Handler : IHandler {
public void Write<T>(Serializer serializer, <#=type.Item2#> data)
{
serializer.Writer.Write(data);
}
public T Read<T>(Deserializer deserializer)
{
return deserializer.Reader.Read<#=type.Item2#>();
}
}
<# } #>
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Wabbajack.Common.Serialization
{
public class Serializer
{
private Dictionary<string, int> _internedStrings = new Dictionary<string, int>();
private Dictionary<Type, HandlerRecord> _handlers = new Dictionary<Type, HandlerRecord>();
public BinaryWriter Writer { get; }
public Serializer(BinaryWriter bw)
{
Writer = bw;
}
public void RegisterWriteHandler<T>(string name, IHandler handler)
{
_handlers.Add(typeof(T), new HandlerRecord
{
TypeName = name,
TypeId = Intern(name),
Handler = handler
});
}
public async Task Write<T>(BinaryWriter bw, T data)
{
var handler = _handlers[typeof(T)];
handler.Handler.Write<T>(this, data);
}
private int Intern(string s)
{
if (_internedStrings.TryGetValue(s, out var idx))
return idx;
idx = _internedStrings.Count;
_internedStrings[s] = idx;
return idx;
}
}
class HandlerRecord
{
public string TypeName;
public int TypeId;
public IHandler Handler;
}
}

View File

@ -32,7 +32,7 @@ using Path = Alphaleonis.Win32.Filesystem.Path;
namespace Wabbajack.Common namespace Wabbajack.Common
{ {
public static class Utils public static partial class Utils
{ {
public static bool IsMO2Running(string mo2Path) public static bool IsMO2Running(string mo2Path)
{ {
@ -206,135 +206,6 @@ namespace Wabbajack.Common
} }
} }
/// <summary>
/// MurMur3 hashes the file pointed to by this string
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public static string FileSHA256(this string file)
{
var sha = new SHA256Managed();
using (var o = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write))
{
using (var i = File.OpenRead(file))
{
i.CopyToWithStatus(new FileInfo(file).Length, o, $"Hashing {Path.GetFileName(file)}");
}
}
return sha.Hash.ToBase64();
}
public static string StringSHA256Hex(this string s)
{
var sha = new SHA256Managed();
using (var o = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write))
{
using var i = new MemoryStream(Encoding.UTF8.GetBytes(s));
i.CopyTo(o);
}
return sha.Hash.ToHex();
}
public static string FileHash(this string file, bool nullOnIOError = false)
{
try
{
var hash = new xxHashConfig();
hash.HashSizeInBits = 64;
hash.Seed = 0x42;
using (var fs = File.OpenRead(file))
{
var config = new xxHashConfig();
config.HashSizeInBits = 64;
using (var f = new StatusFileStream(fs, $"Hashing {Path.GetFileName(file)}"))
{
var value = xxHashFactory.Instance.Create(config).ComputeHash(f);
return value.AsBase64String();
}
}
}
catch (IOException ex)
{
if (nullOnIOError) return null;
throw ex;
}
}
public static string FileHashCached(this string file, bool nullOnIOError = false)
{
if (TryGetHashCache(file, out var foundHash)) return foundHash;
var hash = file.FileHash(nullOnIOError);
if (hash != null)
WriteHashCache(file, hash);
return hash;
}
public static bool TryGetHashCache(string file, out string hash)
{
var hashFile = file + Consts.HashFileExtension;
hash = null;
if (!File.Exists(hashFile)) return false;
if (File.GetSize(hashFile) != 20) return false;
using var fs = File.OpenRead(hashFile);
using var br = new BinaryReader(fs);
var version = br.ReadUInt32();
if (version != HashCacheVersion) return false;
var lastModified = br.ReadUInt64();
if (lastModified != File.GetLastWriteTimeUtc(file).AsUnixTime()) return false;
hash = BitConverter.GetBytes(br.ReadUInt64()).ToBase64();
return true;
}
private const uint HashCacheVersion = 0x01;
private static void WriteHashCache(string file, string hash)
{
using var fs = File.Create(file + Consts.HashFileExtension);
using var bw = new BinaryWriter(fs);
bw.Write(HashCacheVersion);
var lastModified = File.GetLastWriteTimeUtc(file).AsUnixTime();
bw.Write(lastModified);
bw.Write(BitConverter.ToUInt64(hash.FromBase64()));
}
public static async Task<string> FileHashCachedAsync(this string file, bool nullOnIOError = false)
{
if (TryGetHashCache(file, out var foundHash)) return foundHash;
var hash = await file.FileHashAsync(nullOnIOError);
if (hash != null)
WriteHashCache(file, hash);
return hash;
}
public static async Task<string> FileHashAsync(this string file, bool nullOnIOError = false)
{
try
{
var hash = new xxHashConfig();
hash.HashSizeInBits = 64;
hash.Seed = 0x42;
using (var fs = File.OpenRead(file))
{
var config = new xxHashConfig();
config.HashSizeInBits = 64;
var value = await xxHashFactory.Instance.Create(config).ComputeHashAsync(fs);
return value.AsBase64String();
}
}
catch (IOException ex)
{
if (nullOnIOError) return null;
throw ex;
}
}
public static void CopyToWithStatus(this Stream istream, long maxSize, Stream ostream, string status) public static void CopyToWithStatus(this Stream istream, long maxSize, Stream ostream, string status)
{ {
var buffer = new byte[1024 * 64]; var buffer = new byte[1024 * 64];
@ -980,7 +851,7 @@ namespace Wabbajack.Common
} }
} }
public static async Task CreatePatch(FileStream srcStream, string srcHash, FileStream destStream, string destHash, public static async Task CreatePatch(FileStream srcStream, Hash srcHash, FileStream destStream, Hash destHash,
FileStream patchStream) FileStream patchStream)
{ {
await using var sigFile = new TempStream(); await using var sigFile = new TempStream();
@ -996,7 +867,7 @@ namespace Wabbajack.Common
try try
{ {
var cacheFile = Path.Combine(Consts.PatchCacheFolder, $"{srcHash.FromBase64().ToHex()}_{srcHash.FromBase64().ToHex()}.patch"); var cacheFile = Path.Combine(Consts.PatchCacheFolder, $"{srcHash.ToHex()}_{destHash.ToHex()}.patch");
if (!Directory.Exists(Consts.PatchCacheFolder)) if (!Directory.Exists(Consts.PatchCacheFolder))
Directory.CreateDirectory(Consts.PatchCacheFolder); Directory.CreateDirectory(Consts.PatchCacheFolder);
@ -1009,10 +880,10 @@ namespace Wabbajack.Common
} }
} }
public static bool TryGetPatch(string foundHash, string fileHash, out byte[] ePatch) public static bool TryGetPatch(Hash foundHash, Hash fileHash, out byte[] ePatch)
{ {
var patchName = Path.Combine(Consts.PatchCacheFolder, var patchName = Path.Combine(Consts.PatchCacheFolder,
$"{foundHash.FromBase64().ToHex()}_{fileHash.FromBase64().ToHex()}.patch"); $"{foundHash.ToHex()}_{fileHash.ToHex()}.patch");
if (File.Exists(patchName)) if (File.Exists(patchName))
{ {
ePatch = File.ReadAllBytes(patchName); ePatch = File.ReadAllBytes(patchName);

View File

@ -29,6 +29,7 @@
<PackageReference Include="Ceras" Version="4.1.7" /> <PackageReference Include="Ceras" Version="4.1.7" />
<PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" /> <PackageReference Include="Genbox.AlphaFS" Version="2.2.2.1" />
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" /> <PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
<PackageReference Include="MessagePack" Version="2.1.90" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Octodiff" Version="1.2.1" /> <PackageReference Include="Octodiff" Version="1.2.1" />

View File

@ -46,7 +46,7 @@ namespace Wabbajack.Lib
public ModList ModList = new ModList(); public ModList ModList = new ModList();
public List<IndexedArchive> IndexedArchives = new List<IndexedArchive>(); public List<IndexedArchive> IndexedArchives = new List<IndexedArchive>();
public Dictionary<string, IEnumerable<VirtualFile>> IndexedFiles = new Dictionary<string, IEnumerable<VirtualFile>>(); public Dictionary<Hash, IEnumerable<VirtualFile>> IndexedFiles = new Dictionary<Hash, IEnumerable<VirtualFile>>();
public static void Info(string msg) public static void Info(string msg)
{ {
@ -194,25 +194,25 @@ namespace Wabbajack.Lib
{ {
Info("Building a list of archives based on the files required"); Info("Building a list of archives based on the files required");
var shas = InstallDirectives.OfType<FromArchive>() var hashes = InstallDirectives.OfType<FromArchive>()
.Select(a => a.ArchiveHashPath[0]) .Select(a => Hash.FromBase64(a.ArchiveHashPath[0]))
.Distinct(); .Distinct();
var archives = IndexedArchives.OrderByDescending(f => f.File.LastModified) var archives = IndexedArchives.OrderByDescending(f => f.File.LastModified)
.GroupBy(f => f.File.Hash) .GroupBy(f => f.File.Hash)
.ToDictionary(f => f.Key, f => f.First()); .ToDictionary(f => f.Key, f => f.First());
SelectedArchives = await shas.PMap(Queue, sha => ResolveArchive(sha, archives)); SelectedArchives = await hashes.PMap(Queue, hash => ResolveArchive(hash, archives));
} }
public async Task<Archive> ResolveArchive(string sha, IDictionary<string, IndexedArchive> archives) public async Task<Archive> ResolveArchive(Hash hash, IDictionary<Hash, IndexedArchive> archives)
{ {
if (archives.TryGetValue(sha, out var found)) if (archives.TryGetValue(hash, out var found))
{ {
return await ResolveArchive(found); return await ResolveArchive(found);
} }
Error($"No match found for Archive sha: {sha} this shouldn't happen"); Error($"No match found for Archive sha: {hash.ToBase64()} this shouldn't happen");
return null; return null;
} }

View File

@ -27,7 +27,7 @@ namespace Wabbajack.Lib
public string ModListArchive { get; private set; } public string ModListArchive { get; private set; }
public ModList ModList { get; private set; } public ModList ModList { get; private set; }
public Dictionary<string, string> HashedArchives { get; set; } public Dictionary<Hash, string> HashedArchives { get; set; }
public SystemParameters SystemParameters { get; set; } public SystemParameters SystemParameters { get; set; }
@ -127,7 +127,7 @@ namespace Wabbajack.Lib
var grouped = ModList.Directives var grouped = ModList.Directives
.OfType<FromArchive>() .OfType<FromArchive>()
.GroupBy(e => e.ArchiveHashPath[0]) .GroupBy(e => e.ArchiveHashPath[0])
.ToDictionary(k => k.Key); .ToDictionary(k => Hash.FromBase64(k.Key));
var archives = ModList.Archives var archives = ModList.Archives
.Select(a => new { Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash) }) .Select(a => new { Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash) })
.Where(a => a.AbsolutePath != null) .Where(a => a.AbsolutePath != null)
@ -264,7 +264,7 @@ namespace Wabbajack.Lib
{ {
var orig_name = Path.GetFileNameWithoutExtension(archive.Name); var orig_name = Path.GetFileNameWithoutExtension(archive.Name);
var ext = Path.GetExtension(archive.Name); var ext = Path.GetExtension(archive.Name);
var unique_key = archive.State.PrimaryKeyString.StringSHA256Hex(); var unique_key = archive.State.PrimaryKeyString.StringSha256Hex();
outputPath = Path.Combine(DownloadFolder, orig_name + "_" + unique_key + "_" + ext); outputPath = Path.Combine(DownloadFolder, orig_name + "_" + unique_key + "_" + ext);
if (outputPath.FileExists()) if (outputPath.FileExists())
File.Delete(outputPath); File.Delete(outputPath);
@ -413,7 +413,7 @@ namespace Wabbajack.Lib
Utils.Log($"Optimized {ModList.Directives.Count} directives to {indexed.Count} required"); Utils.Log($"Optimized {ModList.Directives.Count} directives to {indexed.Count} required");
var requiredArchives = indexed.Values.OfType<FromArchive>() var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath[0]) .GroupBy(d => d.ArchiveHashPath[0])
.Select(d => d.Key) .Select(d => Hash.FromBase64(d.Key))
.ToHashSet(); .ToHashSet();
ModList.Archives = ModList.Archives.Where(a => requiredArchives.Contains(a.Hash)).ToList(); ModList.Archives = ModList.Archives.Where(a => requiredArchives.Contains(a.Hash)).ToList();

View File

@ -13,10 +13,10 @@ namespace Wabbajack.Lib
return client; return client;
} }
public static async Task<Archive> GetModUpgrade(string hash) public static async Task<Archive> GetModUpgrade(Hash hash)
{ {
using var response = await GetClient() using var response = await GetClient()
.GetAsync($"https://{Consts.WabbajackCacheHostname}/alternative/{hash.FromBase64().ToHex()}"); .GetAsync($"https://{Consts.WabbajackCacheHostname}/alternative/{hash.ToHex()}");
return !response.IsSuccessStatusCode ? null : (await response.Content.ReadAsStringAsync()).FromJSONString<Archive>(); return !response.IsSuccessStatusCode ? null : (await response.Content.ReadAsStringAsync()).FromJSONString<Archive>();
} }
} }

View File

@ -4,13 +4,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem; using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed; using Wabbajack.Common.StatusFeed;
namespace Wabbajack.Lib.CompilationSteps.CompilationErrors namespace Wabbajack.Lib.CompilationSteps.CompilationErrors
{ {
public class InvalidGameESMError : AErrorMessage public class InvalidGameESMError : AErrorMessage
{ {
public string Hash { get; } public Hash Hash { get; }
public string PathToFile { get; } public string PathToFile { get; }
private readonly CleanedESM _esm; private readonly CleanedESM _esm;
public string GameFileName => Path.GetFileName(_esm.To); public string GameFileName => Path.GetFileName(_esm.To);
@ -29,7 +30,7 @@ the modlist expecting a different of the game than you currently have installed,
the game, and then attempting to re-install this modlist. Also verify that the version of the game you have installed matches the version expected by this modlist."; the game, and then attempting to re-install this modlist. Also verify that the version of the game you have installed matches the version expected by this modlist.";
} }
public InvalidGameESMError(CleanedESM esm, string hash, string path) public InvalidGameESMError(CleanedESM esm, Hash hash, string path)
{ {
Hash = hash; Hash = hash;
PathToFile = path; PathToFile = path;

View File

@ -26,7 +26,7 @@ namespace Wabbajack.Lib
public VirtualFile File { get; } public VirtualFile File { get; }
public string Hash => File.Hash; public Hash Hash => File.Hash;
public T EvolveTo<T>() where T : Directive, new() public T EvolveTo<T>() where T : Directive, new()
{ {
@ -130,7 +130,7 @@ namespace Wabbajack.Lib
/// </summary> /// </summary>
public string To; public string To;
public long Size; public long Size;
public string Hash; public Hash Hash;
} }
public class IgnoredDirectly : Directive public class IgnoredDirectly : Directive
@ -167,7 +167,7 @@ namespace Wabbajack.Lib
public class CleanedESM : InlineFile public class CleanedESM : InlineFile
{ {
public string SourceESMHash; public Hash SourceESMHash;
} }
/// <summary> /// <summary>
@ -191,23 +191,13 @@ namespace Wabbajack.Lib
{ {
private string _fullPath; private string _fullPath;
/// <summary>
/// MurMur3 hash of the archive this file comes from
/// </summary>
public string[] ArchiveHashPath; public string[] ArchiveHashPath;
[Exclude] [Exclude]
public VirtualFile FromFile; public VirtualFile FromFile;
[Exclude] [Exclude]
public string FullPath public string FullPath => _fullPath ??= string.Join("|", ArchiveHashPath);
{
get
{
if (_fullPath == null) _fullPath = string.Join("|", ArchiveHashPath);
return _fullPath;
}
}
} }
public class CreateBSA : Directive public class CreateBSA : Directive
@ -226,13 +216,13 @@ namespace Wabbajack.Lib
public string PatchID; public string PatchID;
[Exclude] [Exclude]
public string FromHash; public Hash FromHash;
} }
public class SourcePatch public class SourcePatch
{ {
public string RelativePath; public string RelativePath;
public string Hash; public Hash Hash;
} }
public class MergedPatch : Directive public class MergedPatch : Directive
@ -246,7 +236,7 @@ namespace Wabbajack.Lib
/// <summary> /// <summary>
/// MurMur3 Hash of the archive /// MurMur3 Hash of the archive
/// </summary> /// </summary>
public string Hash { get; set; } public Hash Hash { get; set; }
/// <summary> /// <summary>
/// Meta INI for the downloaded archive /// Meta INI for the downloaded archive

View File

@ -108,7 +108,7 @@ namespace Wabbajack.Lib.Downloaders
var upgradeResult = await Download(upgrade, upgradePath); var upgradeResult = await Download(upgrade, upgradePath);
if (!upgradeResult) return false; if (!upgradeResult) return false;
var patchName = $"{archive.Hash.FromBase64().ToHex()}_{upgrade.Hash.FromBase64().ToHex()}"; var patchName = $"{archive.Hash.ToHex()}_{upgrade.Hash.ToHex()}";
var patchPath = Path.Combine(Path.GetDirectoryName(destination), "_Patch_" + patchName); var patchPath = Path.Combine(Path.GetDirectoryName(destination), "_Patch_" + patchName);
var patchState = new Archive var patchState = new Archive

View File

@ -49,7 +49,7 @@ namespace Wabbajack.Lib.Downloaders
{ {
public Game Game { get; set; } public Game Game { get; set; }
public string GameFile { get; set; } public string GameFile { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
public string GameVersion { get; set; } public string GameVersion { get; set; }

View File

@ -105,7 +105,7 @@ namespace Wabbajack.Lib.FileUploader
if (!tcs.Task.IsFaulted) if (!tcs.Task.IsFaulted)
{ {
progressFn(1.0); progressFn(1.0);
var hash = (await hash_task).FromBase64().ToHex(); var hash = (await hash_task).ToHex();
response = await client.PutAsync(UploadURL + $"/{key}/finish/{hash}", new StringContent("")); response = await client.PutAsync(UploadURL + $"/{key}/finish/{hash}", new StringContent(""));
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
tcs.SetResult(await response.Content.ReadAsStringAsync()); tcs.SetResult(await response.Content.ReadAsStringAsync());

View File

@ -45,7 +45,7 @@ namespace Wabbajack.Lib
public override string VFSCacheName => Path.Combine( public override string VFSCacheName => Path.Combine(
Consts.LocalAppDataPath, Consts.LocalAppDataPath,
$"vfs_compile_cache-{Path.Combine(MO2Folder ?? "Unknown", "ModOrganizer.exe").StringSHA256Hex()}.bin"); $"vfs_compile_cache-{Path.Combine(MO2Folder ?? "Unknown", "ModOrganizer.exe").StringSha256Hex()}.bin");
public MO2Compiler(string mo2Folder, string mo2Profile, string outputFile) public MO2Compiler(string mo2Folder, string mo2Profile, string outputFile)
{ {
@ -318,7 +318,7 @@ namespace Wabbajack.Lib
UpdateTracker.NextStep("Running Validation"); UpdateTracker.NextStep("Running Validation");
await ValidateModlist.RunValidation(Queue, ModList); await ValidateModlist.RunValidation(ModList);
UpdateTracker.NextStep("Generating Report"); UpdateTracker.NextStep("Generating Report");
GenerateManifest(); GenerateManifest();
@ -382,7 +382,7 @@ namespace Wabbajack.Lib
var client = new Common.Http.Client(); var client = new Common.Http.Client();
using var response = using var response =
await client.GetAsync( await client.GetAsync(
$"http://build.wabbajack.org/indexed_files/{vf.Hash.FromBase64().ToHex()}/meta.ini"); $"http://build.wabbajack.org/indexed_files/{vf.Hash.ToHex()}/meta.ini");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {

View File

@ -82,7 +82,7 @@ namespace Wabbajack.Lib
if (cancel.IsCancellationRequested) return false; if (cancel.IsCancellationRequested) return false;
UpdateTracker.NextStep("Validating Modlist"); UpdateTracker.NextStep("Validating Modlist");
await ValidateModlist.RunValidation(Queue, ModList); await ValidateModlist.RunValidation(ModList);
Directory.CreateDirectory(OutputFolder); Directory.CreateDirectory(OutputFolder);
Directory.CreateDirectory(DownloadFolder); Directory.CreateDirectory(DownloadFolder);

View File

@ -96,7 +96,7 @@ namespace Wabbajack.Lib.ModListRegistry
public class DownloadMetadata public class DownloadMetadata
{ {
public string Hash { get; set; } public Hash Hash { get; set; }
public long Size { get; set; } public long Size { get; set; }
public long NumberOfArchives { get; set; } public long NumberOfArchives { get; set; }

View File

@ -16,21 +16,7 @@ namespace Wabbajack.Lib.Validation
/// </summary> /// </summary>
public class ValidateModlist public class ValidateModlist
{ {
public Dictionary<string, Author> AuthorPermissions { get; set; } = new Dictionary<string, Author>(); public ServerWhitelist ServerWhitelist { get; private set; } = new ServerWhitelist();
private readonly WorkQueue _queue;
public ServerWhitelist ServerWhitelist { get; set; } = new ServerWhitelist();
public ValidateModlist(WorkQueue workQueue)
{
_queue = workQueue;
}
public void LoadAuthorPermissionsFromString(string s)
{
AuthorPermissions = s.FromYaml<Dictionary<string, Author>>();
}
public void LoadServerWhitelist(string s) public void LoadServerWhitelist(string s)
{ {
ServerWhitelist = s.FromYaml<ServerWhitelist>(); ServerWhitelist = s.FromYaml<ServerWhitelist>();
@ -50,9 +36,9 @@ namespace Wabbajack.Lib.Validation
} }
public static async Task RunValidation(WorkQueue queue, ModList modlist) public static async Task RunValidation(ModList modlist)
{ {
var validator = new ValidateModlist(queue); var validator = new ValidateModlist();
await validator.LoadListsFromGithub(); await validator.LoadListsFromGithub();
@ -69,92 +55,9 @@ namespace Wabbajack.Lib.Validation
} }
} }
/// <summary>
/// Takes all the permissions for a given Nexus mods and merges them down to a single permissions record
/// the more specific record having precedence in each field.
/// </summary>
/// <param name="mod"></param>
/// <returns></returns>
public Permissions FilePermissions(NexusDownloader.State mod)
{
var author_permissions = AuthorPermissions.GetOrDefault(mod.Author)?.Permissions;
var game_permissions = AuthorPermissions.GetOrDefault(mod.Author)?.Games.GetOrDefault(mod.GameName)?.Permissions;
var mod_permissions = AuthorPermissions.GetOrDefault(mod.Author)?.Games.GetOrDefault(mod.GameName)?.Mods.GetOrDefault(mod.ModID)
?.Permissions;
var file_permissions = AuthorPermissions.GetOrDefault(mod.Author)?.Games.GetOrDefault(mod.GameName)?.Mods
.GetOrDefault(mod.ModID)?.Files.GetOrDefault(mod.FileID)?.Permissions;
return new Permissions
{
CanExtractBSAs = file_permissions?.CanExtractBSAs ?? mod_permissions?.CanExtractBSAs ??
game_permissions?.CanExtractBSAs ?? author_permissions?.CanExtractBSAs ?? true,
CanModifyAssets = file_permissions?.CanModifyAssets ?? mod_permissions?.CanModifyAssets ??
game_permissions?.CanModifyAssets ?? author_permissions?.CanModifyAssets ?? true,
CanModifyESPs = file_permissions?.CanModifyESPs ?? mod_permissions?.CanModifyESPs ??
game_permissions?.CanModifyESPs ?? author_permissions?.CanModifyESPs ?? true,
CanUseInOtherGames = file_permissions?.CanUseInOtherGames ?? mod_permissions?.CanUseInOtherGames ??
game_permissions?.CanUseInOtherGames ?? author_permissions?.CanUseInOtherGames ?? true,
};
}
public async Task<IEnumerable<string>> Validate(ModList modlist) public async Task<IEnumerable<string>> Validate(ModList modlist)
{ {
ConcurrentStack<string> ValidationErrors = new ConcurrentStack<string>(); ConcurrentStack<string> ValidationErrors = new ConcurrentStack<string>();
var nexus_mod_permissions = (await modlist.Archives
.Where(a => a.State is NexusDownloader.State)
.PMap(_queue, a => (a.Hash, FilePermissions((NexusDownloader.State)a.State), a)))
.ToDictionary(a => a.Hash, a => new { permissions = a.Item2, archive = a.a });
await modlist.Directives
.OfType<PatchedFromArchive>()
.PMap(_queue, p =>
{
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{
var ext = Path.GetExtension(p.ArchiveHashPath.Last());
var url = (archive.archive.State as NexusDownloader.State).URL;
if (Consts.AssetFileExtensions.Contains(ext) && !(archive.permissions.CanModifyAssets ?? true))
{
ValidationErrors.Push($"{p.To} from {url} is set to disallow asset modification");
}
else if (Consts.ESPFileExtensions.Contains(ext) && !(archive.permissions.CanModifyESPs ?? true))
{
ValidationErrors.Push($"{p.To} from {url} is set to disallow asset ESP modification");
}
}
});
await modlist.Directives
.OfType<FromArchive>()
.PMap(_queue, p =>
{
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{
var url = (archive.archive.State as NexusDownloader.State).URL;
if (!(archive.permissions.CanExtractBSAs ?? true) &&
p.ArchiveHashPath.Skip(1).ButLast().Any(a => Consts.SupportedBSAs.Contains(Path.GetExtension(a).ToLower())))
{
ValidationErrors.Push($"{p.To} from {url} is set to disallow BSA extraction");
}
}
});
var nexus = NexusApi.NexusApiUtils.ConvertGameName(modlist.GameType.MetaData().NexusName);
modlist.Archives
.Where(a => a.State is NexusDownloader.State)
.Where(m => NexusApi.NexusApiUtils.ConvertGameName(((NexusDownloader.State)m.State).GameName) != nexus)
.Do(m =>
{
var permissions = FilePermissions((NexusDownloader.State)m.State);
if (!(permissions.CanUseInOtherGames ?? true))
{
ValidationErrors.Push(
$"The ModList is for {nexus} but {m.Name} is for game type {((NexusDownloader.State)m.State).GameName} and is not allowed to be converted to other game types");
}
});
modlist.Archives modlist.Archives
.Where(m => !m.State.IsWhitelisted(ServerWhitelist)) .Where(m => !m.State.IsWhitelisted(ServerWhitelist))
.Do(m => .Do(m =>

View File

@ -49,7 +49,7 @@ namespace Wabbajack.Lib
public override string VFSCacheName => Path.Combine( public override string VFSCacheName => Path.Combine(
Consts.LocalAppDataPath, Consts.LocalAppDataPath,
$"vfs_compile_cache-{StagingFolder?.StringSHA256Hex() ?? "Unknown"}.bin"); $"vfs_compile_cache-{StagingFolder?.StringSha256Hex() ?? "Unknown"}.bin");
public VortexCompiler(Game game, string gamePath, string vortexFolder, string downloadsFolder, string stagingFolder, string outputFile) public VortexCompiler(Game game, string gamePath, string vortexFolder, string downloadsFolder, string stagingFolder, string outputFile)
{ {
@ -245,7 +245,7 @@ namespace Wabbajack.Lib
}; };
UpdateTracker.NextStep("Running Validation"); UpdateTracker.NextStep("Running Validation");
await ValidateModlist.RunValidation(Queue, ModList); await ValidateModlist.RunValidation(ModList);
UpdateTracker.NextStep("Generating Report"); UpdateTracker.NextStep("Generating Report");
GenerateManifest(); GenerateManifest();

View File

@ -52,8 +52,7 @@ namespace Wabbajack.Test
public void TestSetup() public void TestSetup()
{ {
queue = new WorkQueue(); queue = new WorkQueue();
validate = new ValidateModlist(queue); validate = new ValidateModlist();
validate.LoadAuthorPermissionsFromString(permissions);
validate.LoadServerWhitelist(server_whitelist); validate.LoadServerWhitelist(server_whitelist);
} }
@ -63,76 +62,6 @@ namespace Wabbajack.Test
queue?.Dispose(); queue?.Dispose();
} }
[TestMethod]
public void TestRightsFallthrough()
{
var permissions = validate.FilePermissions(new NexusDownloader.State
{
Author = "bill",
GameName = "Skyrim",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsFalse();
permissions.CanUseInOtherGames.AssertIsFalse();
permissions = validate.FilePermissions(new NexusDownloader.State
{
Author = "bob",
GameName = "Skyrim",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsTrue();
permissions.CanModifyESPs.AssertIsTrue();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusDownloader.State
{
Author = "bill",
GameName = "Fallout4",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsTrue();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusDownloader.State
{
Author = "bill",
GameName = "Skyrim",
ModID = "43",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusDownloader.State
{
Author = "bill",
GameName = "Skyrim",
ModID = "42",
FileID = "31"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsFalse();
permissions.CanUseInOtherGames.AssertIsTrue();
}
[TestMethod] [TestMethod]
public async Task TestModValidation() public async Task TestModValidation()
{ {
@ -150,9 +79,8 @@ namespace Wabbajack.Test
ModID = "42", ModID = "42",
FileID = "33", FileID = "33",
}, },
Hash = "DEADBEEF" Hash = Hash.FromLong(42)
} }
}, },
Directives = new List<Directive> Directives = new List<Directive>
{ {
@ -163,59 +91,14 @@ namespace Wabbajack.Test
} }
} }
}; };
IEnumerable<string> errors;
// No errors, simple archive extraction
errors = await validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 0);
// Error due to patched file
modlist.Directives[0] = new PatchedFromArchive
{
PatchID = Guid.NewGuid().ToString(),
ArchiveHashPath = new[] {"DEADBEEF", "foo\\bar\\baz.pex"},
};
errors = await validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1);
// Error due to extracted BSA file
modlist.Directives[0] = new FromArchive
{
ArchiveHashPath = new[] { "DEADBEEF", "foo.bsa", "foo\\bar\\baz.dds" },
};
errors = await validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1);
// No error since we're just installing the .bsa, not extracting it
modlist.Directives[0] = new FromArchive
{
ArchiveHashPath = new[] { "DEADBEEF", "foo.bsa"},
};
errors = await validate.Validate(modlist);
Assert.AreEqual(0, errors.Count());
// Error due to game conversion
modlist.GameType = Game.SkyrimSpecialEdition;
modlist.Directives[0] = new FromArchive
{
ArchiveHashPath = new[] { "DEADBEEF", "foo\\bar\\baz.dds" },
};
errors = await validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1);
// Error due to file downloaded from 3rd party // Error due to file downloaded from 3rd party
modlist.GameType = Game.Skyrim; modlist.GameType = Game.Skyrim;
modlist.Archives[0] = new Archive() modlist.Archives[0] = new Archive()
{ {
State = new HTTPDownloader.State() { Url = "https://somebadplace.com" }, State = new HTTPDownloader.State() { Url = "https://somebadplace.com" },
Hash = "DEADBEEF" Hash = Hash.FromLong(42)
}; };
errors = await validate.Validate(modlist); var errors = await validate.Validate(modlist);
Assert.AreEqual(1, errors.Count()); Assert.AreEqual(1, errors.Count());
// Ok due to file downloaded from whitelisted 3rd party // Ok due to file downloaded from whitelisted 3rd party
@ -223,7 +106,7 @@ namespace Wabbajack.Test
modlist.Archives[0] = new Archive modlist.Archives[0] = new Archive
{ {
State = new HTTPDownloader.State { Url = "https://somegoodplace.com/baz.7z" }, State = new HTTPDownloader.State { Url = "https://somegoodplace.com/baz.7z" },
Hash = "DEADBEEF" Hash = Hash.FromLong(42)
}; };
errors = await validate.Validate(modlist); errors = await validate.Validate(modlist);
Assert.AreEqual(0, errors.Count()); Assert.AreEqual(0, errors.Count());
@ -234,7 +117,7 @@ namespace Wabbajack.Test
modlist.Archives[0] = new Archive modlist.Archives[0] = new Archive
{ {
State = new GoogleDriveDownloader.State { Id = "bleg"}, State = new GoogleDriveDownloader.State { Id = "bleg"},
Hash = "DEADBEEF" Hash = Hash.FromLong(42)
}; };
errors = await validate.Validate(modlist); errors = await validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1); Assert.AreEqual(errors.Count(), 1);
@ -244,7 +127,7 @@ namespace Wabbajack.Test
modlist.Archives[0] = new Archive modlist.Archives[0] = new Archive
{ {
State = new GoogleDriveDownloader.State { Id = "googleDEADBEEF" }, State = new GoogleDriveDownloader.State { Id = "googleDEADBEEF" },
Hash = "DEADBEEF" Hash = Hash.FromLong(42)
}; };
errors = await validate.Validate(modlist); errors = await validate.Validate(modlist);
Assert.AreEqual(0, errors.Count()); Assert.AreEqual(0, errors.Count());
@ -256,7 +139,7 @@ namespace Wabbajack.Test
{ {
using (var workQueue = new WorkQueue()) using (var workQueue = new WorkQueue())
{ {
await new ValidateModlist(workQueue).LoadListsFromGithub(); await new ValidateModlist().LoadListsFromGithub();
} }
} }
} }

View File

@ -536,7 +536,7 @@ namespace Wabbajack.Test
var archive = new Archive var archive = new Archive
{ {
Name = "Cori.7z", Name = "Cori.7z",
Hash = "gCRVrvzDNH0=", Hash = Hash.FromBase64("gCRVrvzDNH0="),
State = new NexusDownloader.State State = new NexusDownloader.State
{ {
GameName = Game.SkyrimSpecialEdition.MetaData().NexusName, GameName = Game.SkyrimSpecialEdition.MetaData().NexusName,

View File

@ -82,7 +82,7 @@ namespace Wabbajack.VirtualFileSystem.Test
await AddTestRoot(); await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="]; var files = context.Index.ByHash[Hash.FromBase64("qX0GZvIaTKM=")];
Assert.AreEqual(files.Count(), 2); Assert.AreEqual(files.Count(), 2);
} }
@ -150,7 +150,7 @@ namespace Wabbajack.VirtualFileSystem.Test
await AddTestRoot(); await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="]; var files = context.Index.ByHash[Hash.FromBase64("qX0GZvIaTKM=")];
var cleanup = await context.Stage(files); var cleanup = await context.Stage(files);
@ -173,7 +173,7 @@ namespace Wabbajack.VirtualFileSystem.Test
await AddTestRoot(); await AddTestRoot();
var files = context.Index.ByHash["qX0GZvIaTKM="]; var files = context.Index.ByHash[Hash.FromBase64("qX0GZvIaTKM=")];
var archive = context.Index.ByRootPath[Path.Combine(VFS_TEST_DIR_FULL, "test.zip")]; var archive = context.Index.ByRootPath[Path.Combine(VFS_TEST_DIR_FULL, "test.zip")];
var state = context.GetPortableState(files); var state = context.GetPortableState(files);
@ -181,9 +181,9 @@ namespace Wabbajack.VirtualFileSystem.Test
var new_context = new Context(Queue); var new_context = new Context(Queue);
await new_context.IntegrateFromPortable(state, await new_context.IntegrateFromPortable(state,
new Dictionary<string, string> {{archive.Hash, archive.FullPath}}); new Dictionary<Hash, string> {{archive.Hash, archive.FullPath}});
var new_files = new_context.Index.ByHash["qX0GZvIaTKM="]; var new_files = new_context.Index.ByHash[Hash.FromBase64("qX0GZvIaTKM=")];
var close = await new_context.Stage(new_files); var close = await new_context.Stage(new_files);

View File

@ -249,16 +249,16 @@ namespace Wabbajack.VirtualFileSystem
{ {
Name = f.Parent != null ? f.Name : null, Name = f.Parent != null ? f.Name : null,
Hash = f.Hash, Hash = f.Hash,
ParentHash = f.Parent?.Hash, ParentHash = f.Parent?.Hash ?? Hash.Empty,
Size = f.Size Size = f.Size
}).ToList(); }).ToList();
} }
public async Task IntegrateFromPortable(List<PortableFile> state, Dictionary<string, string> links) public async Task IntegrateFromPortable(List<PortableFile> state, Dictionary<Hash, string> links)
{ {
var indexedState = state.GroupBy(f => f.ParentHash) var indexedState = state.GroupBy(f => f.ParentHash)
.ToDictionary(f => f.Key ?? "", f => (IEnumerable<PortableFile>) f); .ToDictionary(f => f.Key, f => (IEnumerable<PortableFile>) f);
var parents = await indexedState[""] var parents = await indexedState[Hash.Empty]
.PMap(Queue,f => VirtualFile.CreateFromPortable(this, indexedState, links, f)); .PMap(Queue,f => VirtualFile.CreateFromPortable(this, indexedState, links, f));
var newIndex = await Index.Integrate(parents); var newIndex = await Index.Integrate(parents);
@ -297,7 +297,7 @@ namespace Wabbajack.VirtualFileSystem
void BackFillOne(KnownFile file) void BackFillOne(KnownFile file)
{ {
var parent = newFiles[file.Paths[0]]; var parent = newFiles[Hash.FromBase64(file.Paths[0])];
foreach (var path in file.Paths.Skip(1)) foreach (var path in file.Paths.Skip(1))
{ {
if (parentchild.TryGetValue((parent, path), out var foundParent)) if (parentchild.TryGetValue((parent, path), out var foundParent))
@ -331,7 +331,7 @@ namespace Wabbajack.VirtualFileSystem
public class KnownFile public class KnownFile
{ {
public string[] Paths { get; set; } public string[] Paths { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
} }
public class DisposableList<T> : List<T>, IDisposable public class DisposableList<T> : List<T>, IDisposable
@ -355,7 +355,7 @@ namespace Wabbajack.VirtualFileSystem
public IndexRoot(ImmutableList<VirtualFile> aFiles, public IndexRoot(ImmutableList<VirtualFile> aFiles,
ImmutableDictionary<string, VirtualFile> byFullPath, ImmutableDictionary<string, VirtualFile> byFullPath,
ImmutableDictionary<string, ImmutableStack<VirtualFile>> byHash, ImmutableDictionary<Hash, ImmutableStack<VirtualFile>> byHash,
ImmutableDictionary<string, VirtualFile> byRoot, ImmutableDictionary<string, VirtualFile> byRoot,
ImmutableDictionary<string, ImmutableStack<VirtualFile>> byName) ImmutableDictionary<string, ImmutableStack<VirtualFile>> byName)
{ {
@ -370,7 +370,7 @@ namespace Wabbajack.VirtualFileSystem
{ {
AllFiles = ImmutableList<VirtualFile>.Empty; AllFiles = ImmutableList<VirtualFile>.Empty;
ByFullPath = ImmutableDictionary<string, VirtualFile>.Empty; ByFullPath = ImmutableDictionary<string, VirtualFile>.Empty;
ByHash = ImmutableDictionary<string, ImmutableStack<VirtualFile>>.Empty; ByHash = ImmutableDictionary<Hash, ImmutableStack<VirtualFile>>.Empty;
ByRootPath = ImmutableDictionary<string, VirtualFile>.Empty; ByRootPath = ImmutableDictionary<string, VirtualFile>.Empty;
ByName = ImmutableDictionary<string, ImmutableStack<VirtualFile>>.Empty; ByName = ImmutableDictionary<string, ImmutableStack<VirtualFile>>.Empty;
} }
@ -378,7 +378,7 @@ namespace Wabbajack.VirtualFileSystem
public ImmutableList<VirtualFile> AllFiles { get; } public ImmutableList<VirtualFile> AllFiles { get; }
public ImmutableDictionary<string, VirtualFile> ByFullPath { get; } public ImmutableDictionary<string, VirtualFile> ByFullPath { get; }
public ImmutableDictionary<string, ImmutableStack<VirtualFile>> ByHash { get; } public ImmutableDictionary<Hash, ImmutableStack<VirtualFile>> ByHash { get; }
public ImmutableDictionary<string, ImmutableStack<VirtualFile>> ByName { get; set; } public ImmutableDictionary<string, ImmutableStack<VirtualFile>> ByName { get; set; }
public ImmutableDictionary<string, VirtualFile> ByRootPath { get; } public ImmutableDictionary<string, VirtualFile> ByRootPath { get; }
@ -391,7 +391,7 @@ namespace Wabbajack.VirtualFileSystem
.ToImmutableDictionary(f => f.FullPath)); .ToImmutableDictionary(f => f.FullPath));
var byHash = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren) var byHash = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren)
.Where(f => f.Hash != null) .Where(f => f.Hash != Hash.Empty)
.ToGroupedImmutableDictionary(f => f.Hash)); .ToGroupedImmutableDictionary(f => f.Hash));
var byName = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren) var byName = Task.Run(() => allFiles.SelectMany(f => f.ThisAndAllChildren)
@ -410,7 +410,7 @@ namespace Wabbajack.VirtualFileSystem
public VirtualFile FileForArchiveHashPath(string[] argArchiveHashPath) public VirtualFile FileForArchiveHashPath(string[] argArchiveHashPath)
{ {
var cur = ByHash[argArchiveHashPath[0]].First(f => f.Parent == null); var cur = ByHash[Hash.FromBase64(argArchiveHashPath[0])].First(f => f.Parent == null);
return argArchiveHashPath.Skip(1).Aggregate(cur, (current, itm) => ByName[itm].First(f => f.Parent == current)); return argArchiveHashPath.Skip(1).Aggregate(cur, (current, itm) => ByName[itm].First(f => f.Parent == current));
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Wabbajack.Common;
namespace Wabbajack.VirtualFileSystem namespace Wabbajack.VirtualFileSystem
{ {
@ -8,7 +9,7 @@ namespace Wabbajack.VirtualFileSystem
public class IndexedVirtualFile public class IndexedVirtualFile
{ {
public string Name { get; set; } public string Name { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
public long Size { get; set; } public long Size { get; set; }
public List<IndexedVirtualFile> Children { get; set; } = new List<IndexedVirtualFile>(); public List<IndexedVirtualFile> Children { get; set; } = new List<IndexedVirtualFile>();
} }

View File

@ -1,10 +1,12 @@
namespace Wabbajack.VirtualFileSystem using Wabbajack.Common;
namespace Wabbajack.VirtualFileSystem
{ {
public class PortableFile public class PortableFile
{ {
public string Name { get; set; } public string Name { get; set; }
public string Hash { get; set; } public Hash Hash { get; set; }
public string ParentHash { get; set; } public Hash ParentHash { get; set; }
public long Size { get; set; } public long Size { get; set; }
} }
} }

View File

@ -42,7 +42,7 @@ namespace Wabbajack.VirtualFileSystem
} }
} }
public string Hash { get; internal set; } public Hash Hash { get; internal set; }
public ExtendedHashes ExtendedHashes { get; set; } public ExtendedHashes ExtendedHashes { get; set; }
public long Size { get; internal set; } public long Size { get; internal set; }
@ -214,12 +214,12 @@ namespace Wabbajack.VirtualFileSystem
return self; return self;
} }
private static async Task<IndexedVirtualFile> TryGetContentsFromServer(string hash) private static async Task<IndexedVirtualFile> TryGetContentsFromServer(Hash hash)
{ {
try try
{ {
var client = new HttpClient(); var client = new HttpClient();
var response = await client.GetAsync($"http://{Consts.WabbajackCacheHostname}/indexed_files/{hash.FromBase64().ToHex()}"); var response = await client.GetAsync($"http://{Consts.WabbajackCacheHostname}/indexed_files/{hash.ToHex()}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
return null; return null;
@ -274,7 +274,7 @@ namespace Wabbajack.VirtualFileSystem
Parent = parent, Parent = parent,
Name = br.ReadString(), Name = br.ReadString(),
_fullPath = br.ReadString(), _fullPath = br.ReadString(),
Hash = br.ReadString(), Hash = br.ReadHash(),
Size = br.ReadInt64(), Size = br.ReadInt64(),
LastModified = br.ReadInt64(), LastModified = br.ReadInt64(),
LastAnalyzed = br.ReadInt64(), LastAnalyzed = br.ReadInt64(),
@ -288,7 +288,7 @@ namespace Wabbajack.VirtualFileSystem
} }
public static VirtualFile CreateFromPortable(Context context, public static VirtualFile CreateFromPortable(Context context,
Dictionary<string, IEnumerable<PortableFile>> state, Dictionary<string, string> links, Dictionary<Hash, IEnumerable<PortableFile>> state, Dictionary<Hash, string> links,
PortableFile portableFile) PortableFile portableFile)
{ {
var vf = new VirtualFile var vf = new VirtualFile
@ -305,7 +305,7 @@ namespace Wabbajack.VirtualFileSystem
} }
public static VirtualFile CreateFromPortable(Context context, VirtualFile parent, public static VirtualFile CreateFromPortable(Context context, VirtualFile parent,
Dictionary<string, IEnumerable<PortableFile>> state, PortableFile portableFile) Dictionary<Hash, IEnumerable<PortableFile>> state, PortableFile portableFile)
{ {
var vf = new VirtualFile var vf = new VirtualFile
{ {
@ -323,7 +323,7 @@ namespace Wabbajack.VirtualFileSystem
public string[] MakeRelativePaths() public string[] MakeRelativePaths()
{ {
var path = new string[NestingFactor]; var path = new string[NestingFactor];
path[0] = FilesInFullPath.First().Hash; path[0] = FilesInFullPath.First().Hash.ToBase64();
var idx = 1; var idx = 1;

View File

@ -65,19 +65,19 @@ namespace Wabbajack
return list return list
// Sort randomly initially, just to give each list a fair shake // Sort randomly initially, just to give each list a fair shake
.Shuffle(random) .Shuffle(random)
.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}"); .AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? Hash.Empty);
} }
catch (Exception ex) catch (Exception ex)
{ {
Utils.Error(ex); Utils.Error(ex);
Error = ErrorResponse.Fail(ex); Error = ErrorResponse.Fail(ex);
return Observable.Empty<IChangeSet<ModlistMetadata, string>>(); return Observable.Empty<IChangeSet<ModlistMetadata, Hash>>();
} }
}) })
// Unsubscribe and release when not active // Unsubscribe and release when not active
.FlowSwitch( .FlowSwitch(
this.WhenAny(x => x.IsActive), this.WhenAny(x => x.IsActive),
valueWhenOff: Observable.Return(ChangeSet<ModlistMetadata, string>.Empty)) valueWhenOff: Observable.Return(ChangeSet<ModlistMetadata, Hash>.Empty))
.Switch() .Switch()
.RefCount(); .RefCount();

View File

@ -92,7 +92,7 @@ namespace Wabbajack
return Order(Archives.Where(x => return Order(Archives.Where(x =>
{ {
if (term.StartsWith("hash:")) if (term.StartsWith("hash:"))
return x.Hash.StartsWith(term.Replace("hash:", "")); return x.Hash.ToString().StartsWith(term.Replace("hash:", ""));
return x.Name.StartsWith(term); return x.Name.StartsWith(term);
})); }));
}) })