diff --git a/Netkan/CKAN-netkan.csproj b/Netkan/CKAN-netkan.csproj
index ba8a389f46..2d1ad797bc 100644
--- a/Netkan/CKAN-netkan.csproj
+++ b/Netkan/CKAN-netkan.csproj
@@ -46,6 +46,8 @@
+
+
@@ -71,7 +73,9 @@
+
+
@@ -129,6 +133,7 @@
+
@@ -172,4 +177,4 @@
-
\ No newline at end of file
+
diff --git a/Netkan/Processors/Inflator.cs b/Netkan/Processors/Inflator.cs
index 22b82bf327..1bec2be1dc 100644
--- a/Netkan/Processors/Inflator.cs
+++ b/Netkan/Processors/Inflator.cs
@@ -26,9 +26,11 @@ public Inflator(string cacheDir, bool overwriteCache, string githubToken, bool p
IModuleService moduleService = new ModuleService();
IFileService fileService = new FileService(cache);
+ IConfigParser configParser = new CachingConfigParser(moduleService);
http = new CachingHttpService(cache, overwriteCache);
- ckanValidator = new CkanValidator(http, moduleService);
- transformer = new NetkanTransformer(http, fileService, moduleService, githubToken, prerelease, netkanValidator);
+ ckanValidator = new CkanValidator(http, moduleService, configParser);
+ transformer = new NetkanTransformer(http, fileService, moduleService, configParser,
+ githubToken, prerelease, netkanValidator);
}
internal IEnumerable Inflate(string filename, Metadata netkan, TransformOptions opts)
diff --git a/Netkan/Services/CachingConfigParser.cs b/Netkan/Services/CachingConfigParser.cs
new file mode 100644
index 0000000000..16b1be3278
--- /dev/null
+++ b/Netkan/Services/CachingConfigParser.cs
@@ -0,0 +1,87 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+
+using log4net;
+using ICSharpCode.SharpZipLib.Zip;
+using ParsecSharp;
+using KSPMMCfgParser;
+using static KSPMMCfgParser.KSPMMCfgParser;
+
+namespace CKAN.NetKAN.Services
+{
+ using NodeCache = Dictionary;
+
+ ///
+ /// Since parsing cfg files can be expensive, cache results for 15 minutes
+ ///
+ internal sealed class CachingConfigParser : IConfigParser
+ {
+ public CachingConfigParser(IModuleService modSvc)
+ {
+ moduleService = modSvc;
+ }
+
+ public Dictionary GetConfigNodes(CkanModule module, ZipFile zip, GameInstance inst)
+ => GetCachedNodes(module) ?? AddAndReturn(
+ module,
+ moduleService.GetConfigFiles(module, zip, inst).ToDictionary(
+ cfg => cfg,
+ cfg => ConfigFile.ToArray()
+ .Parse(zip.GetInputStream(cfg.source))
+ .CaseOf(failure =>
+ {
+ log.InfoFormat("{0}:{1}:{2}: {3}",
+ inst.ToRelativeGameDir(cfg.destination),
+ failure.State.Position.Line,
+ failure.State.Position.Column,
+ failure.Message);
+ return new KSPConfigNode[] { };
+ },
+ success => success.Value)));
+
+ private Dictionary AddAndReturn(CkanModule module,
+ Dictionary nodes)
+ {
+ log.DebugFormat("Caching config nodes for {0}", module);
+ cache.Add(module,
+ new ConfigNodesCacheEntry()
+ {
+ Value = nodes,
+ Timestamp = DateTime.Now,
+ });
+ return nodes;
+ }
+
+ private Dictionary GetCachedNodes(CkanModule module)
+ {
+ if (cache.TryGetValue(module, out ConfigNodesCacheEntry entry))
+ {
+ if (DateTime.Now - entry.Timestamp < stringCacheLifetime)
+ {
+ log.DebugFormat("Using cached nodes for {0}", module);
+ return entry.Value;
+ }
+ else
+ {
+ log.DebugFormat("Purging stale nodes for {0}", module);
+ cache.Remove(module);
+ }
+ }
+ return null;
+ }
+
+ private readonly IModuleService moduleService;
+ private readonly NodeCache cache = new NodeCache();
+ // Re-use parse results within 15 minutes
+ private static readonly TimeSpan stringCacheLifetime = new TimeSpan(0, 15, 0);
+ private static readonly ILog log = LogManager.GetLogger(typeof(CachingConfigParser));
+ }
+
+ public class ConfigNodesCacheEntry
+ {
+ public Dictionary Value;
+ public DateTime Timestamp;
+ }
+}
diff --git a/Netkan/Services/IConfigParser.cs b/Netkan/Services/IConfigParser.cs
new file mode 100644
index 0000000000..a42824a407
--- /dev/null
+++ b/Netkan/Services/IConfigParser.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+using ICSharpCode.SharpZipLib.Zip;
+using KSPMMCfgParser;
+
+namespace CKAN.NetKAN.Services
+{
+ internal interface IConfigParser
+ {
+ Dictionary GetConfigNodes(CkanModule module, ZipFile zip, GameInstance inst);
+ }
+}
diff --git a/Netkan/Transformers/LocalizationsTransformer.cs b/Netkan/Transformers/LocalizationsTransformer.cs
index 731d897dee..fe654092c4 100644
--- a/Netkan/Transformers/LocalizationsTransformer.cs
+++ b/Netkan/Transformers/LocalizationsTransformer.cs
@@ -1,10 +1,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
+
using ICSharpCode.SharpZipLib.Zip;
using log4net;
using Newtonsoft.Json.Linq;
+
using CKAN.Extensions;
using CKAN.NetKAN.Extensions;
using CKAN.NetKAN.Model;
@@ -21,10 +22,11 @@ internal sealed class LocalizationsTransformer : ITransformer
///
/// HTTP service
/// Module service
- public LocalizationsTransformer(IHttpService http, IModuleService moduleService)
+ public LocalizationsTransformer(IHttpService http, IModuleService moduleService, IConfigParser parser)
{
_http = http;
_moduleService = moduleService;
+ _parser = parser;
}
///
@@ -56,16 +58,13 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts)
log.Debug("Extracting locales");
// Extract the locale names from the ZIP's cfg files
- var locales = _moduleService.GetConfigFiles(mod, zip, inst)
- .Select(cfg => new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd())
- .SelectMany(contents => localizationRegex.Matches(contents).Cast()
- .Select(m => m.Groups["contents"].Value))
- .SelectMany(contents => localeRegex.Matches(contents).Cast()
- .Where(m => m.Groups["contents"].Value.Contains("="))
- .Select(m => m.Groups["locale"].Value))
- .Distinct()
- .OrderBy(l => l)
- .Memoize();
+ var locales = _parser.GetConfigNodes(mod, zip, inst)
+ .SelectMany(kvp => kvp.Value)
+ .Where(node => node.Name == localizationsNodeName)
+ .SelectMany(node => node.Children.Select(child => child.Name))
+ .Distinct()
+ .OrderBy(l => l)
+ .Memoize();
log.Debug("Locales extracted");
if (locales.Any())
@@ -82,20 +81,13 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts)
}
}
+ private const string localizationsNodeName = "Localization";
private const string localizationsProperty = "localizations";
private readonly IHttpService _http;
private readonly IModuleService _moduleService;
+ private readonly IConfigParser _parser;
private static readonly ILog log = LogManager.GetLogger(typeof(LocalizationsTransformer));
-
- private static readonly Regex localizationRegex = new Regex(
- @"^\s*Localization\b\s*{(?[^{}]+({[^{}]*}[^{}]*)+)}",
- RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
- );
- private static readonly Regex localeRegex = new Regex(
- @"^\s*(?[-a-zA-Z]+).*?{(?.*?)}",
- RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
- );
}
}
diff --git a/Netkan/Transformers/NetkanTransformer.cs b/Netkan/Transformers/NetkanTransformer.cs
index fd8b5d9563..2e28ed297c 100644
--- a/Netkan/Transformers/NetkanTransformer.cs
+++ b/Netkan/Transformers/NetkanTransformer.cs
@@ -24,6 +24,7 @@ public NetkanTransformer(
IHttpService http,
IFileService fileService,
IModuleService moduleService,
+ IConfigParser configParser,
string githubToken,
bool prerelease,
IValidator validator
@@ -43,7 +44,7 @@ IValidator validator
new AvcKrefTransformer(http, ghApi),
new InternalCkanTransformer(http, moduleService),
new AvcTransformer(http, moduleService, ghApi),
- new LocalizationsTransformer(http, moduleService),
+ new LocalizationsTransformer(http, moduleService, configParser),
new VersionEditTransformer(),
new ForcedVTransformer(),
new EpochTransformer(),
diff --git a/Netkan/Validators/CkanValidator.cs b/Netkan/Validators/CkanValidator.cs
index 53018e8f2e..854d90e7d7 100644
--- a/Netkan/Validators/CkanValidator.cs
+++ b/Netkan/Validators/CkanValidator.cs
@@ -8,10 +8,8 @@ internal sealed class CkanValidator : IValidator
{
private readonly List _validators;
- public CkanValidator(IHttpService downloader, IModuleService moduleService)
+ public CkanValidator(IHttpService downloader, IModuleService moduleService, IConfigParser configParser)
{
- this.downloader = downloader;
- this.moduleService = moduleService;
_validators = new List
{
new IsCkanModuleValidator(),
@@ -21,8 +19,9 @@ public CkanValidator(IHttpService downloader, IModuleService moduleService)
new ObeysCKANSchemaValidator(),
new KindValidator(),
new HarmonyValidator(downloader, moduleService),
- new ModuleManagerDependsValidator(downloader, moduleService),
- new PluginsValidator(downloader, moduleService),
+ new ModuleManagerDependsValidator(downloader, moduleService, configParser),
+ new PluginsValidator(downloader, moduleService, configParser),
+ new ForClauseValidator(downloader, moduleService, configParser),
new CraftsInShipsValidator(downloader, moduleService),
};
}
@@ -40,8 +39,5 @@ public void ValidateCkan(Metadata metadata, Metadata netkan)
Validate(metadata);
new MatchingIdentifiersValidator(netkan.Identifier).Validate(metadata);
}
-
- private IHttpService downloader;
- private IModuleService moduleService;
}
}
diff --git a/Netkan/Validators/ForClauseValidator.cs b/Netkan/Validators/ForClauseValidator.cs
new file mode 100644
index 0000000000..6168698742
--- /dev/null
+++ b/Netkan/Validators/ForClauseValidator.cs
@@ -0,0 +1,60 @@
+using System.Linq;
+
+using Newtonsoft.Json.Linq;
+using ICSharpCode.SharpZipLib.Zip;
+using log4net;
+
+using CKAN.NetKAN.Services;
+using CKAN.NetKAN.Model;
+using CKAN.Games;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class ForClauseValidator : IValidator
+ {
+ public ForClauseValidator(IHttpService http, IModuleService moduleService, IConfigParser parser)
+ {
+ _http = http;
+ _moduleService = moduleService;
+ _parser = parser;
+ }
+
+ public void Validate(Metadata metadata)
+ {
+ Log.Info("Validating that :FOR[] clauses specify the right mod");
+
+ JObject json = metadata.Json();
+ CkanModule mod = CkanModule.FromJson(json.ToString());
+ if (!mod.IsDLC)
+ {
+ var package = _http.DownloadModule(metadata);
+ if (!string.IsNullOrEmpty(package))
+ {
+ ZipFile zip = new ZipFile(package);
+ GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser());
+
+ // Check for :FOR[identifier] in .cfg files
+ var mismatchedIdentifiers = KerbalSpaceProgram
+ .IdentifiersFromConfigNodes(
+ _parser.GetConfigNodes(mod, zip, inst)
+ .SelectMany(kvp => kvp.Value))
+ .Where(ident => ident != mod.identifier
+ && Identifier.ValidIdentifierPattern.IsMatch(ident))
+ .OrderBy(s => s)
+ .ToArray();
+ if (mismatchedIdentifiers.Any())
+ {
+ Log.WarnFormat("Found :FOR[] clauses with the wrong identifiers: {0}",
+ string.Join(", ", mismatchedIdentifiers));
+ }
+ }
+ }
+ }
+
+ private readonly IHttpService _http;
+ private readonly IModuleService _moduleService;
+ private readonly IConfigParser _parser;
+
+ private static readonly ILog Log = LogManager.GetLogger(typeof(ForClauseValidator));
+ }
+}
diff --git a/Netkan/Validators/ModuleManagerDependsValidator.cs b/Netkan/Validators/ModuleManagerDependsValidator.cs
index 58d1e9cbda..65c5747cda 100644
--- a/Netkan/Validators/ModuleManagerDependsValidator.cs
+++ b/Netkan/Validators/ModuleManagerDependsValidator.cs
@@ -1,9 +1,12 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+
using Newtonsoft.Json.Linq;
using ICSharpCode.SharpZipLib.Zip;
using log4net;
+using KSPMMCfgParser;
+
using CKAN.NetKAN.Services;
using CKAN.NetKAN.Model;
using CKAN.Extensions;
@@ -13,10 +16,11 @@ namespace CKAN.NetKAN.Validators
{
internal sealed class ModuleManagerDependsValidator : IValidator
{
- public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService)
+ public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService, IConfigParser parser)
{
_http = http;
_moduleService = moduleService;
+ _parser = parser;
}
public void Validate(Metadata metadata)
@@ -32,10 +36,10 @@ public void Validate(Metadata metadata)
{
ZipFile zip = new ZipFile(package);
GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser());
- var mmConfigs = _moduleService.GetConfigFiles(mod, zip, inst)
- .Where(cfg => moduleManagerRegex.IsMatch(
- new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd()))
- .Memoize();
+ var mmConfigs = _parser.GetConfigNodes(mod, zip, inst)
+ .Where(kvp => kvp.Value.Any(node => HasAnyModuleManager(node)))
+ .Select(kvp => kvp.Key)
+ .ToArray();
bool dependsOnMM = mod?.depends?.Any(r => r.ContainsAny(identifiers)) ?? false;
@@ -56,13 +60,25 @@ public void Validate(Metadata metadata)
private string[] identifiers = new string[] { "ModuleManager" };
- private static readonly Regex moduleManagerRegex = new Regex(
- @"^\s*[@+$\-!%]|^\s*[a-zA-Z0-9_]+:",
- RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
- );
+ private static bool HasAnyModuleManager(KSPConfigNode node)
+ => node.Operator != MMOperator.Insert
+ || node.Filters != null
+ || node.Needs != null
+ || node.Has != null
+ || node.Index != null
+ || node.Properties.Any(prop => HasAnyModuleManager(prop))
+ || node.Children.Any( child => HasAnyModuleManager(child));
+
+ private static bool HasAnyModuleManager(KSPConfigProperty prop)
+ => prop.Operator != MMOperator.Insert
+ || prop.Needs != null
+ || prop.Index != null
+ || prop.ArrayIndex != null
+ || prop.AssignmentOperator != null;
private readonly IHttpService _http;
private readonly IModuleService _moduleService;
+ private readonly IConfigParser _parser;
private static readonly ILog Log = LogManager.GetLogger(typeof(ModuleManagerDependsValidator));
}
diff --git a/Netkan/Validators/PluginsValidator.cs b/Netkan/Validators/PluginsValidator.cs
index c4e25f3dc1..46f7e1c5db 100644
--- a/Netkan/Validators/PluginsValidator.cs
+++ b/Netkan/Validators/PluginsValidator.cs
@@ -1,9 +1,10 @@
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
+
using Newtonsoft.Json.Linq;
using ICSharpCode.SharpZipLib.Zip;
using log4net;
+
using CKAN.Extensions;
using CKAN.NetKAN.Services;
using CKAN.NetKAN.Model;
@@ -13,10 +14,11 @@ namespace CKAN.NetKAN.Validators
{
internal sealed class PluginsValidator : IValidator
{
- public PluginsValidator(IHttpService http, IModuleService moduleService)
+ public PluginsValidator(IHttpService http, IModuleService moduleService, IConfigParser parser)
{
_http = http;
_moduleService = moduleService;
+ _parser = parser;
}
public void Validate(Metadata metadata)
@@ -42,15 +44,22 @@ public void Validate(Metadata metadata)
.OrderBy(f => f)
.ToList();
var dllIdentifiers = dllPaths
- .Select(p => inst.DllPathToIdentifier(p))
+ .SelectMany(p => inst.game.IdentifiersFromFileName(inst, p))
.Where(ident => !string.IsNullOrEmpty(ident)
&& !identifiersToIgnore.Contains(ident))
.ToHashSet();
if (dllIdentifiers.Any() && !dllIdentifiers.Contains(metadata.Identifier))
{
- Log.WarnFormat(
- "No plugin matching the identifier, manual installations won't be detected: {0}",
- string.Join(", ", dllPaths));
+ // Check for :FOR[identifier] in .cfg files
+ var cfgIdentifiers = KerbalSpaceProgram.IdentifiersFromConfigNodes(
+ _parser.GetConfigNodes(mod, zip, inst)
+ .SelectMany(kvp => kvp.Value));
+ if (!cfgIdentifiers.Contains(metadata.Identifier))
+ {
+ Log.WarnFormat(
+ "No plugin or :FOR[] clause matching the identifier, manual installations won't be detected: {0}",
+ string.Join(", ", dllPaths));
+ }
}
bool boundedCompatibility = json.ContainsKey("ksp_version") || json.ContainsKey("ksp_version_max");
@@ -76,6 +85,7 @@ public void Validate(Metadata metadata)
private readonly IHttpService _http;
private readonly IModuleService _moduleService;
+ private readonly IConfigParser _parser;
private static readonly ILog Log = LogManager.GetLogger(typeof(PluginsValidator));
}
diff --git a/Tests/NetKAN/Validators/CkanValidatorTests.cs b/Tests/NetKAN/Validators/CkanValidatorTests.cs
index 5a27a64565..7678f10538 100644
--- a/Tests/NetKAN/Validators/CkanValidatorTests.cs
+++ b/Tests/NetKAN/Validators/CkanValidatorTests.cs
@@ -33,11 +33,12 @@ public void DoesNotThrowOnValidCkan()
// Arrange
var mHttp = new Mock();
var mModuleService = new Mock();
+ var mConfigParser = new Mock();
mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny()))
.Returns(true);
- var sut = new CkanValidator(mHttp.Object, mModuleService.Object);
+ var sut = new CkanValidator(mHttp.Object, mModuleService.Object, mConfigParser.Object);
var json = (JObject)ValidCkan.DeepClone();
// Act
@@ -58,11 +59,12 @@ public void DoesThrowWhenMissingProperty(string propertyName)
// Arrange
var mHttp = new Mock();
var mModuleService = new Mock();
+ var mConfigParser = new Mock();
mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny()))
.Returns(true);
- var sut = new CkanValidator(mHttp.Object, mModuleService.Object);
+ var sut = new CkanValidator(mHttp.Object, mModuleService.Object, mConfigParser.Object);
var json = (JObject)ValidCkan.DeepClone();
json.Remove(propertyName);
@@ -81,11 +83,12 @@ public void DoesThrowWhenIdentifiersDoNotMatch()
// Arrange
var mHttp = new Mock();
var mModuleService = new Mock();
+ var mConfigParser = new Mock();
mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny()))
.Returns(true);
- var sut = new CkanValidator(mHttp.Object, mModuleService.Object);
+ var sut = new CkanValidator(mHttp.Object, mModuleService.Object, mConfigParser.Object);
var json = new JObject();
json["spec_version"] = 1;
json["identifier"] = "AmazingMod";
@@ -105,6 +108,7 @@ public void DoesThrowWhenNoInstallableFiles()
// Arrange
var mHttp = new Mock();
var mModuleService = new Mock();
+ var mConfigParser = new Mock();
mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny()))
.Returns(false);
@@ -113,7 +117,7 @@ public void DoesThrowWhenNoInstallableFiles()
netkan["spec_version"] = 1;
netkan["identifier"] = "AwesomeMod";
- var sut = new CkanValidator(mHttp.Object, mModuleService.Object);
+ var sut = new CkanValidator(mHttp.Object, mModuleService.Object, mConfigParser.Object);
var json = (JObject)ValidCkan.DeepClone();
// Act