implement framework for content rights management

This commit is contained in:
Timothy Baldridge 2019-09-28 22:41:47 -06:00
parent 45bc7ec62d
commit 6c24e4fdbb
10 changed files with 375 additions and 0 deletions

View File

@ -18,6 +18,8 @@ namespace Wabbajack.Common
public static HashSet<string> SupportedBSAs = new HashSet<string> {".bsa"};
public static HashSet<string> ConfigFileExtensions = new HashSet<string> {".json", ".ini", ".yml"};
public static HashSet<string> ESPFileExtensions = new HashSet<string>() { ".esp", ".esm", ".esl"};
public static HashSet<string> AssetFileExtensions = new HashSet<string>() {".dds", ".tga", ".nif", ".psc", ".pex"};
public static string NexusCacheDirectory = "nexus_link_cache";

View File

@ -485,6 +485,11 @@ namespace Wabbajack.Common
Log($"WARNING: {s}");
}
public static TV GetOrDefault<TK, TV>(this Dictionary<TK, TV> dict, TK key)
{
return dict.TryGetValue(key, out var result) ? result : default;
}
public static byte[] ConcatArrays(this IEnumerable<byte[]> arrays)
{
var outarr = new byte[arrays.Sum(a => a.Length)];

View File

@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Validation;
using Game = Wabbajack.Common.Game;
namespace Wabbajack.Test
{
[TestClass]
public class ContentRightsManagementTests
{
private ValidateModlist validate;
private static string permissions = @"
bill:
Permissions:
CanExtractBSAs: false
Games:
Skyrim:
Permissions:
CanModifyESPs: false
Mods:
42:
Permissions:
CanModifyAssets: false
Files:
33:
Permissions:
CanUseInOtherGames: false
";
[TestInitialize]
public void TestSetup()
{
WorkQueue.Init((x, y, z) => { }, (min, max) => { });
validate = new ValidateModlist();
validate.LoadAuthorPermissionsFromString(permissions);
}
[TestMethod]
public void TestRightsFallthrough()
{
var permissions = validate.FilePermissions(new NexusMod()
{
Author = "bill",
GameName = "Skyrim",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsFalse();
permissions.CanUseInOtherGames.AssertIsFalse();
permissions = validate.FilePermissions(new NexusMod()
{
Author = "bob",
GameName = "Skyrim",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsTrue();
permissions.CanModifyESPs.AssertIsTrue();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusMod()
{
Author = "bill",
GameName = "Fallout4",
ModID = "42",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsTrue();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusMod()
{
Author = "bill",
GameName = "Skyrim",
ModID = "43",
FileID = "33"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsTrue();
permissions.CanUseInOtherGames.AssertIsTrue();
permissions = validate.FilePermissions(new NexusMod()
{
Author = "bill",
GameName = "Skyrim",
ModID = "42",
FileID = "31"
});
permissions.CanExtractBSAs.AssertIsFalse();
permissions.CanModifyESPs.AssertIsFalse();
permissions.CanModifyAssets.AssertIsFalse();
permissions.CanUseInOtherGames.AssertIsTrue();
}
[TestMethod]
public void TestModValidation()
{
var modlist = new ModList
{
GameType = Game.Skyrim,
Archives = new List<Archive>
{
new NexusMod
{
GameName = "Skyrim",
Author = "bill",
ModID = "42",
FileID = "33",
Hash = "DEADBEEF"
}
},
Directives = new List<Directive>
{
new FromArchive
{
ArchiveHashPath = new[] {"DEADBEEF", "foo\\bar\\baz.pex"},
To = "foo\\bar\\baz.pex"
}
}
};
IEnumerable<string> errors;
// No errors, simple archive extraction
errors = validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 0);
// Error due to patched file
modlist.Directives[0] = new PatchedFromArchive
{
Patch = new byte[]{0, 1, 3},
ArchiveHashPath = new[] {"DEADBEEF", "foo\\bar\\baz.pex"},
};
errors = 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 = validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1);
// Error due to game conversion
modlist.GameType = Game.SkyrimSpecialEdition;
modlist.Directives[0] = new FromArchive
{
ArchiveHashPath = new[] { "DEADBEEF", "foo\\bar\\baz.dds" },
};
errors = validate.Validate(modlist);
Assert.AreEqual(errors.Count(), 1);
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Wabbajack.Test
{
public static class Extensions
{
public static void AssertIsFalse(this bool? condition)
{
Assert.IsFalse(condition ?? true, string.Empty, (object[])null);
}
public static void AssertIsTrue(this bool? condition)
{
Assert.IsTrue(condition ?? false, string.Empty, (object[])null);
}
}
}

View File

@ -73,9 +73,11 @@
<Reference Include="System.Transactions" />
</ItemGroup>
<ItemGroup>
<Compile Include="Extensions.cs" />
<Compile Include="TestUtils.cs" />
<Compile Include="SanityTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ContentRightsManagementTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using VFS;
using Wabbajack.Common;
namespace Wabbajack
{
@ -38,6 +39,11 @@ namespace Wabbajack
/// </summary>
public List<Archive> Archives;
/// <summary>
/// The game variant to which this game applies
/// </summary>
public Game GameType;
/// <summary>
/// Install directives
/// </summary>

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack.Validation
{
public class Permissions
{
public bool? CanExtractBSAs { get; set; }
public bool? CanModifyESPs { get; set; }
public bool? CanModifyAssets { get; set; }
public bool? CanUseInOtherGames { get; set; }
}
public class Author
{
public Permissions Permissions { get; set; }
public Dictionary<string, Game> Games;
}
public class Game
{
public Permissions Permissions;
public Dictionary<string, Mod> Mods;
}
public class Mod
{
public Permissions Permissions;
public Dictionary<string, File> Files;
}
public class File
{
public Permissions Permissions;
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Wabbajack.Validation
{
/// <summary>
/// Core class for rights management. Given a Wabbajack modlist this class will return a list of all the
/// know rights violations of the modlist
/// </summary>
public class ValidateModlist
{
public Dictionary<string, Author> AuthorPermissions { get; set; }
public void LoadAuthorPermissionsFromString(string s)
{
var d = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
AuthorPermissions = d.Deserialize<Dictionary<string, Author>>(s);
}
/// <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(NexusMod 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 IEnumerable<string> Validate(ModList modlist)
{
ConcurrentStack<string> ValidationErrors = new ConcurrentStack<string>();
var nexus_mod_permissions = modlist.Archives
.OfType<NexusMod>()
.PMap(a => (a.Hash, FilePermissions(a), a))
.ToDictionary(a => a.Hash, a => new { permissions = a.Item2, archive = a.a});
modlist.Directives
.OfType<PatchedFromArchive>()
.PMap(p =>
{
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{
var ext = Path.GetExtension(p.ArchiveHashPath.Last());
if (Consts.AssetFileExtensions.Contains(ext) && !(archive.permissions.CanModifyAssets ?? true))
{
ValidationErrors.Push($"{p.To} from {archive.archive.NexusURL} is set to disallow asset modification");
}
else if (Consts.ESPFileExtensions.Contains(ext) && !(archive.permissions.CanModifyESPs ?? true))
{
ValidationErrors.Push($"{p.To} from {archive.archive.NexusURL} is set to disallow asset ESP modification");
}
}
});
modlist.Directives
.OfType<FromArchive>()
.PMap(p =>
{
if (nexus_mod_permissions.TryGetValue(p.ArchiveHashPath[0], out var archive))
{
if (!(archive.permissions.CanExtractBSAs ?? true) &&
p.ArchiveHashPath.Skip(1).Any(a => Consts.SupportedBSAs.Contains(Path.GetExtension(a))))
{
ValidationErrors.Push($"{p.To} from {archive.archive.NexusURL} is set to disallow BSA Extraction");
}
}
});
var nexus = NexusApi.NexusApiUtils.ConvertGameName(GameRegistry.Games[modlist.GameType].NexusName);
modlist.Archives
.OfType<NexusMod>()
.Where(m => m.GameName.ToLower() != nexus)
.Do(m => ValidationErrors.Push($"The modlist is for {nexus} but {m.Name} is for game type {m.GameName} and is not allowed to be converted to other game types"));
return ValidationErrors.ToList();
}
}
}

View File

@ -174,6 +174,9 @@
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="YamlDotNet, Version=7.0.0.0, Culture=neutral, PublicKeyToken=ec19458f3c15af5e, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.7.0.0\lib\net45\YamlDotNet.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
@ -191,6 +194,8 @@
<Compile Include="Themes\LeftMarginMultiplierConverter.cs" />
<Compile Include="Themes\TreeViewItemExtensions.cs" />
<Compile Include="UIUtils.cs" />
<Compile Include="Validation\DTOs.cs" />
<Compile Include="Validation\ValidateModlist.cs" />
<Compile Include="zEditIntegration.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
@ -291,6 +296,7 @@
<EmbeddedResource Include="Icons\github.png" />
<EmbeddedResource Include="Icons\patreon.png" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Fody.5.1.1\build\Fody.targets" Condition="Exists('..\packages\Fody.5.1.1\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@ -22,4 +22,5 @@
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net472" />
<package id="WebSocketSharpFork" version="1.0.4.0" targetFramework="net472" />
<package id="WPFThemes.DarkBlend" version="1.0.8" targetFramework="net472" />
<package id="YamlDotNet" version="7.0.0" targetFramework="net472" />
</packages>