Skip to content

Commit 199d4c8

Browse files
committed
Use cfg parser for Netkan
1 parent 8d68856 commit 199d4c8

12 files changed

Lines changed: 252 additions & 61 deletions

File tree

Netkan/CKAN-netkan.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<PackageReference Include="Namotion.Reflection" Version="1.0.7" />
4747
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
4848
<PackageReference Include="YamlDotNet" Version="9.1.0" />
49+
<PackageReference Include="ParsecSharp" Version="3.4.0" PrivateAssets="All" />
4950
</ItemGroup>
5051
<ItemGroup>
5152
<Reference Include="System" />
@@ -71,7 +72,9 @@
7172
<Compile Include="Properties\AssemblyInfo.cs" />
7273
<Compile Include="QueueAppender.cs" />
7374
<Compile Include="Services\CachingHttpService.cs" />
75+
<Compile Include="Services\CachingConfigParser.cs" />
7476
<Compile Include="Services\FileService.cs" />
77+
<Compile Include="Services\IConfigParser.cs" />
7578
<Compile Include="Services\IFileService.cs" />
7679
<Compile Include="Services\IHttpService.cs" />
7780
<Compile Include="Services\IModuleService.cs" />
@@ -129,6 +132,7 @@
129132
<Compile Include="Validators\CkanValidator.cs" />
130133
<Compile Include="Validators\CraftsInShipsValidator.cs" />
131134
<Compile Include="Validators\DownloadVersionValidator.cs" />
135+
<Compile Include="Validators\ForClauseValidator.cs" />
132136
<Compile Include="Validators\HasIdentifierValidator.cs" />
133137
<Compile Include="Validators\HarmonyValidator.cs" />
134138
<Compile Include="Validators\InstallsFilesValidator.cs" />

Netkan/Processors/Inflator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ public Inflator(string cacheDir, bool overwriteCache, string githubToken, bool p
2626

2727
IModuleService moduleService = new ModuleService();
2828
IFileService fileService = new FileService(cache);
29+
IConfigParser configParser = new CachingConfigParser(moduleService);
2930
http = new CachingHttpService(cache, overwriteCache);
30-
ckanValidator = new CkanValidator(http, moduleService);
31-
transformer = new NetkanTransformer(http, fileService, moduleService, githubToken, prerelease, netkanValidator);
31+
ckanValidator = new CkanValidator(http, moduleService, configParser);
32+
transformer = new NetkanTransformer(http, fileService, moduleService, configParser,
33+
githubToken, prerelease, netkanValidator);
3234
}
3335

3436
internal IEnumerable<Metadata> Inflate(string filename, Metadata netkan, TransformOptions opts)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Collections.Generic;
5+
6+
using log4net;
7+
using ICSharpCode.SharpZipLib.Zip;
8+
using ParsecSharp;
9+
10+
namespace CKAN.NetKAN.Services
11+
{
12+
using NodeCache = Dictionary<CkanModule, ConfigNodesCacheEntry>;
13+
14+
/// <summary>
15+
/// Since parsing cfg files can be expensive, cache results for 15 minutes
16+
/// </summary>
17+
internal sealed class CachingConfigParser : IConfigParser
18+
{
19+
public CachingConfigParser(IModuleService modSvc)
20+
{
21+
moduleService = modSvc;
22+
}
23+
24+
public Dictionary<InstallableFile, KSPConfigNode[]> GetConfigNodes(CkanModule module, ZipFile zip, GameInstance inst)
25+
=> GetCachedNodes(module) ?? AddAndReturn(
26+
module,
27+
moduleService.GetConfigFiles(module, zip, inst).ToDictionary(
28+
cfg => cfg,
29+
cfg => KSPConfigParser.ConfigFile.ToArray()
30+
.Parse(zip.GetInputStream(cfg.source))
31+
.CaseOf(failure =>
32+
{
33+
log.InfoFormat("{0}:{1}:{2}: {3}",
34+
inst.ToRelativeGameDir(cfg.destination),
35+
failure.State.Position.Line,
36+
failure.State.Position.Column,
37+
failure.Message);
38+
return new KSPConfigNode[] { };
39+
},
40+
success => success.Value)));
41+
42+
private Dictionary<InstallableFile, KSPConfigNode[]> AddAndReturn(CkanModule module,
43+
Dictionary<InstallableFile, KSPConfigNode[]> nodes)
44+
{
45+
log.DebugFormat("Caching config nodes for {0}", module);
46+
cache.Add(module,
47+
new ConfigNodesCacheEntry()
48+
{
49+
Value = nodes,
50+
Timestamp = DateTime.Now,
51+
});
52+
return nodes;
53+
}
54+
55+
private Dictionary<InstallableFile, KSPConfigNode[]> GetCachedNodes(CkanModule module)
56+
{
57+
if (cache.TryGetValue(module, out ConfigNodesCacheEntry entry))
58+
{
59+
if (DateTime.Now - entry.Timestamp < stringCacheLifetime)
60+
{
61+
log.DebugFormat("Using cached nodes for {0}", module);
62+
return entry.Value;
63+
}
64+
else
65+
{
66+
log.DebugFormat("Purging stale nodes for {0}", module);
67+
cache.Remove(module);
68+
}
69+
}
70+
return null;
71+
}
72+
73+
private readonly IModuleService moduleService;
74+
private readonly NodeCache cache = new NodeCache();
75+
// Re-use parse results within 15 minutes
76+
private static readonly TimeSpan stringCacheLifetime = new TimeSpan(0, 15, 0);
77+
private static readonly ILog log = LogManager.GetLogger(typeof(CachingConfigParser));
78+
}
79+
80+
public class ConfigNodesCacheEntry
81+
{
82+
public Dictionary<InstallableFile, KSPConfigNode[]> Value;
83+
public DateTime Timestamp;
84+
}
85+
}

Netkan/Services/IConfigParser.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
using ICSharpCode.SharpZipLib.Zip;
5+
6+
namespace CKAN.NetKAN.Services
7+
{
8+
internal interface IConfigParser
9+
{
10+
Dictionary<InstallableFile, KSPConfigNode[]> GetConfigNodes(CkanModule module, ZipFile zip, GameInstance inst);
11+
}
12+
}

Netkan/Transformers/LocalizationsTransformer.cs

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.Collections.Generic;
22
using System.IO;
33
using System.Linq;
4-
using System.Text.RegularExpressions;
4+
55
using ICSharpCode.SharpZipLib.Zip;
66
using log4net;
77
using Newtonsoft.Json.Linq;
8+
89
using CKAN.Extensions;
910
using CKAN.NetKAN.Extensions;
1011
using CKAN.NetKAN.Model;
@@ -21,10 +22,11 @@ internal sealed class LocalizationsTransformer : ITransformer
2122
/// </summary>
2223
/// <param name="http">HTTP service</param>
2324
/// <param name="moduleService">Module service</param>
24-
public LocalizationsTransformer(IHttpService http, IModuleService moduleService)
25+
public LocalizationsTransformer(IHttpService http, IModuleService moduleService, IConfigParser parser)
2526
{
2627
_http = http;
2728
_moduleService = moduleService;
29+
_parser = parser;
2830
}
2931

3032
/// <summary>
@@ -56,16 +58,13 @@ public IEnumerable<Metadata> Transform(Metadata metadata, TransformOptions opts)
5658

5759
log.Debug("Extracting locales");
5860
// Extract the locale names from the ZIP's cfg files
59-
var locales = _moduleService.GetConfigFiles(mod, zip, inst)
60-
.Select(cfg => new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd())
61-
.SelectMany(contents => localizationRegex.Matches(contents).Cast<Match>()
62-
.Select(m => m.Groups["contents"].Value))
63-
.SelectMany(contents => localeRegex.Matches(contents).Cast<Match>()
64-
.Where(m => m.Groups["contents"].Value.Contains("="))
65-
.Select(m => m.Groups["locale"].Value))
66-
.Distinct()
67-
.OrderBy(l => l)
68-
.Memoize();
61+
var locales = _parser.GetConfigNodes(mod, zip, inst)
62+
.SelectMany(kvp => kvp.Value)
63+
.Where(node => node.Name == localizationsNodeName)
64+
.SelectMany(node => node.Children.Select(child => child.Name))
65+
.Distinct()
66+
.OrderBy(l => l)
67+
.Memoize();
6968
log.Debug("Locales extracted");
7069

7170
if (locales.Any())
@@ -82,20 +81,13 @@ public IEnumerable<Metadata> Transform(Metadata metadata, TransformOptions opts)
8281
}
8382
}
8483

84+
private const string localizationsNodeName = "Localization";
8585
private const string localizationsProperty = "localizations";
8686

8787
private readonly IHttpService _http;
8888
private readonly IModuleService _moduleService;
89+
private readonly IConfigParser _parser;
8990

9091
private static readonly ILog log = LogManager.GetLogger(typeof(LocalizationsTransformer));
91-
92-
private static readonly Regex localizationRegex = new Regex(
93-
@"^\s*Localization\b\s*{(?<contents>[^{}]+({[^{}]*}[^{}]*)+)}",
94-
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
95-
);
96-
private static readonly Regex localeRegex = new Regex(
97-
@"^\s*(?<locale>[-a-zA-Z]+).*?{(?<contents>.*?)}",
98-
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
99-
);
10092
}
10193
}

Netkan/Transformers/NetkanTransformer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public NetkanTransformer(
2424
IHttpService http,
2525
IFileService fileService,
2626
IModuleService moduleService,
27+
IConfigParser configParser,
2728
string githubToken,
2829
bool prerelease,
2930
IValidator validator
@@ -43,7 +44,7 @@ IValidator validator
4344
new AvcKrefTransformer(http, ghApi),
4445
new InternalCkanTransformer(http, moduleService),
4546
new AvcTransformer(http, moduleService, ghApi),
46-
new LocalizationsTransformer(http, moduleService),
47+
new LocalizationsTransformer(http, moduleService, configParser),
4748
new VersionEditTransformer(),
4849
new ForcedVTransformer(),
4950
new EpochTransformer(),

Netkan/Validators/CkanValidator.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ internal sealed class CkanValidator : IValidator
88
{
99
private readonly List<IValidator> _validators;
1010

11-
public CkanValidator(IHttpService downloader, IModuleService moduleService)
11+
public CkanValidator(IHttpService downloader, IModuleService moduleService, IConfigParser configParser)
1212
{
13-
this.downloader = downloader;
14-
this.moduleService = moduleService;
1513
_validators = new List<IValidator>
1614
{
1715
new IsCkanModuleValidator(),
@@ -21,8 +19,9 @@ public CkanValidator(IHttpService downloader, IModuleService moduleService)
2119
new ObeysCKANSchemaValidator(),
2220
new KindValidator(),
2321
new HarmonyValidator(downloader, moduleService),
24-
new ModuleManagerDependsValidator(downloader, moduleService),
25-
new PluginsValidator(downloader, moduleService),
22+
new ModuleManagerDependsValidator(downloader, moduleService, configParser),
23+
new PluginsValidator(downloader, moduleService, configParser),
24+
new ForClauseValidator(downloader, moduleService, configParser),
2625
new CraftsInShipsValidator(downloader, moduleService),
2726
};
2827
}
@@ -40,8 +39,5 @@ public void ValidateCkan(Metadata metadata, Metadata netkan)
4039
Validate(metadata);
4140
new MatchingIdentifiersValidator(netkan.Identifier).Validate(metadata);
4241
}
43-
44-
private IHttpService downloader;
45-
private IModuleService moduleService;
4642
}
4743
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Linq;
2+
3+
using Newtonsoft.Json.Linq;
4+
using ICSharpCode.SharpZipLib.Zip;
5+
using log4net;
6+
7+
using CKAN.NetKAN.Services;
8+
using CKAN.NetKAN.Model;
9+
using CKAN.Games;
10+
11+
namespace CKAN.NetKAN.Validators
12+
{
13+
internal sealed class ForClauseValidator : IValidator
14+
{
15+
public ForClauseValidator(IHttpService http, IModuleService moduleService, IConfigParser parser)
16+
{
17+
_http = http;
18+
_moduleService = moduleService;
19+
_parser = parser;
20+
}
21+
22+
public void Validate(Metadata metadata)
23+
{
24+
Log.Info("Validating that :FOR[] clauses specify the right mod");
25+
26+
JObject json = metadata.Json();
27+
CkanModule mod = CkanModule.FromJson(json.ToString());
28+
if (!mod.IsDLC)
29+
{
30+
var package = _http.DownloadModule(metadata);
31+
if (!string.IsNullOrEmpty(package))
32+
{
33+
ZipFile zip = new ZipFile(package);
34+
GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser());
35+
36+
// Check for :FOR[identifier] in .cfg files
37+
var mismatchedIdentifiers = KerbalSpaceProgram
38+
.IdentifiersFromConfigNodes(
39+
_parser.GetConfigNodes(mod, zip, inst)
40+
.SelectMany(kvp => kvp.Value))
41+
.Where(ident => ident != mod.identifier
42+
&& Identifier.ValidIdentifierPattern.IsMatch(ident))
43+
.OrderBy(s => s)
44+
.ToArray();
45+
if (mismatchedIdentifiers.Any())
46+
{
47+
Log.WarnFormat("Found :FOR[] clauses with the wrong identifiers: {0}",
48+
string.Join(", ", mismatchedIdentifiers));
49+
}
50+
}
51+
}
52+
}
53+
54+
private readonly IHttpService _http;
55+
private readonly IModuleService _moduleService;
56+
private readonly IConfigParser _parser;
57+
58+
private static readonly ILog Log = LogManager.GetLogger(typeof(ForClauseValidator));
59+
}
60+
}

Netkan/Validators/ModuleManagerDependsValidator.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System.IO;
22
using System.Linq;
33
using System.Text.RegularExpressions;
4+
45
using Newtonsoft.Json.Linq;
56
using ICSharpCode.SharpZipLib.Zip;
67
using log4net;
8+
79
using CKAN.NetKAN.Services;
810
using CKAN.NetKAN.Model;
911
using CKAN.Extensions;
@@ -13,10 +15,11 @@ namespace CKAN.NetKAN.Validators
1315
{
1416
internal sealed class ModuleManagerDependsValidator : IValidator
1517
{
16-
public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService)
18+
public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService, IConfigParser parser)
1719
{
1820
_http = http;
1921
_moduleService = moduleService;
22+
_parser = parser;
2023
}
2124

2225
public void Validate(Metadata metadata)
@@ -32,10 +35,10 @@ public void Validate(Metadata metadata)
3235
{
3336
ZipFile zip = new ZipFile(package);
3437
GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser());
35-
var mmConfigs = _moduleService.GetConfigFiles(mod, zip, inst)
36-
.Where(cfg => moduleManagerRegex.IsMatch(
37-
new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd()))
38-
.Memoize();
38+
var mmConfigs = _parser.GetConfigNodes(mod, zip, inst)
39+
.Where(kvp => kvp.Value.Any(node => HasAnyModuleManager(node)))
40+
.Select(kvp => kvp.Key)
41+
.ToArray();
3942

4043
bool dependsOnMM = mod?.depends?.Any(r => r.ContainsAny(identifiers)) ?? false;
4144

@@ -56,13 +59,25 @@ public void Validate(Metadata metadata)
5659

5760
private string[] identifiers = new string[] { "ModuleManager" };
5861

59-
private static readonly Regex moduleManagerRegex = new Regex(
60-
@"^\s*[@+$\-!%]|^\s*[a-zA-Z0-9_]+:",
61-
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline
62-
);
62+
private static bool HasAnyModuleManager(KSPConfigNode node)
63+
=> node.Operator != MMOperator.Insert
64+
|| node.Filters != null
65+
|| node.Needs != null
66+
|| node.Has != null
67+
|| node.Index != null
68+
|| node.Properties.Any(prop => HasAnyModuleManager(prop))
69+
|| node.Children.Any( child => HasAnyModuleManager(child));
70+
71+
private static bool HasAnyModuleManager(KSPConfigProperty prop)
72+
=> prop.Operator != MMOperator.Insert
73+
|| prop.Needs != null
74+
|| prop.Index != null
75+
|| prop.ArrayIndex != null
76+
|| prop.AssignmentOperator != null;
6377

6478
private readonly IHttpService _http;
6579
private readonly IModuleService _moduleService;
80+
private readonly IConfigParser _parser;
6681

6782
private static readonly ILog Log = LogManager.GetLogger(typeof(ModuleManagerDependsValidator));
6883
}

0 commit comments

Comments
 (0)