2using System.Collections.Generic;
3using System.Collections.Immutable;
5using System.Text.RegularExpressions;
7using IKVM.ByteCode.Decoding;
20 internal class ModulePath : ModuleFinder
29 public new static IModuleFinder Create(ImmutableArray<string> entries)
31 if (entries.IsDefault)
32 throw new ArgumentNullException(nameof(entries));
34 return new ModulePath(JarFile.RUNTIME_VERSION, entries);
44 public static IModuleFinder Create(RuntimeVersion version, ImmutableArray<string> entries)
46 return new ModulePath(version, entries);
49 const string MODULE_INFO =
"module-info.class";
50 const string AUTOMATIC_MODULE_NAME =
"Automatic-Module-Name";
52 readonly RuntimeVersion _runtimeVersion;
53 readonly ImmutableArray<string> _entries;
54 readonly Dictionary<string, ModuleReference> _cachedModules =
new();
56 ImmutableHashSet<ModuleReference>? _all;
64 public ModulePath(RuntimeVersion releaseVersion, ImmutableArray<string> entries)
66 if (entries.IsDefault)
67 throw new ArgumentException(
"Uninitialized immutable array.", nameof(entries));
69 _runtimeVersion = releaseVersion;
74 public override ModuleReference? Find(
string name)
77 throw new ArgumentNullException(nameof(name));
79 var m = _cachedModules.GetValueOrDefault(name);
83 while (HasNextEntry())
86 m = _cachedModules.GetValueOrDefault(name);
95 public override ImmutableHashSet<ModuleReference> FindAll()
100 _all = [.. _cachedModules.Values];
111 while (HasNextEntry())
121 return _next < _entries.Length;
131 var modules = Scan(_entries[_next]);
135 foreach (var kvp
in modules)
136 _cachedModules.TryAdd(kvp.Key, kvp.Value);
146 Dictionary<string, ModuleReference> Scan(
string entry)
150 if (Directory.Exists(entry))
152 var mi = Path.Combine(entry, MODULE_INFO);
153 if (File.Exists(mi) ==
false)
154 return ScanDirectory(entry);
157 var mref = ReadModule(entry);
158 if (mref is not
null)
159 return new Dictionary<string, ModuleReference>() { [mref.Descriptor.Name] = mref };
161 throw new FindException($
"Module format not recognized: {entry}");
163 catch (IOException e)
165 throw new FindException(e);
175 Dictionary<string, ModuleReference> ScanDirectory(
string dir)
177 var nameToReference =
new Dictionary<string, ModuleReference>();
179 foreach (var entry
in Directory.EnumerateFileSystemEntries(dir))
181 var mref = ReadModule(entry);
182 if (mref is not
null)
184 var name = mref.Descriptor.Name;
185 if (nameToReference.TryGetValue(name, out var prev))
186 throw new FindException($
"Two versions of module {name} found in {dir} ({mref.Location} and {prev.Location}).");
188 nameToReference.Add(name, mref);
192 return nameToReference;
202 ModuleReference? ReadModule(
string path)
205 throw new ArgumentNullException(nameof(path));
207 path = Path.GetFullPath(path);
212 if (Directory.Exists(path))
213 return ReadExplodedModule(path);
216 if (File.Exists(path))
217 if (Path.GetExtension(path) ==
".jar")
218 return ReadJar(path);
222 catch (InvalidModuleDescriptorException e)
224 throw new FindException($
"Error reading module: {path}", e);
233 ModuleReference? ReadExplodedModule(
string dir)
236 throw new ArgumentNullException(nameof(dir));
238 dir = Path.GetFullPath(dir);
239 var moduleInfoFile =
new FileInfo(Path.Combine(dir, MODULE_INFO));
240 if (moduleInfoFile.Exists ==
false)
243 using var desc = moduleInfoFile.Open(FileMode.Open);
244 using var clzz = ClassFile.Read(desc);
245 return ModuleReference.CreateExplodedModule(ModuleDescriptor.Read(clzz), _runtimeVersion, dir);
253 ModuleReference ReadJar(
string file)
255 using var jar =
new JarFile(file, _runtimeVersion);
257 var moduleInfoEntry = jar.GetEntry(MODULE_INFO);
258 if (moduleInfoEntry is
null)
262 return ModuleReference.CreateJarModule(DeriveModuleDescriptor(jar, file), _runtimeVersion, file);
266 throw new FindException($
"Unable to derive module descriptor for '{file}'.", e);
271 using var desc = moduleInfoEntry.Value.Open();
272 using var clzz = ClassFile.Read(desc);
273 return ModuleReference.CreateJarModule(ModuleDescriptor.Read(clzz), _runtimeVersion, file);
290 ModuleDescriptor DeriveModuleDescriptor(JarFile jar,
string file)
292 string? moduleName =
null;
295 var man = jar.Manifest;
296 if (man is not
null && man.MainAttributes.TryGetValue(AUTOMATIC_MODULE_NAME, out var amn))
300 GetModuleNameAndVersionFromFileName(file, out var fmn, out var version);
301 if (moduleName is
null && fmn is not
null)
305 if (moduleName is
null)
306 throw new FindException($
"Could not derive module name from '{file}'.");
309 var builder = ModuleDescriptor.CreateAutomaticModule(moduleName);
311 builder = builder.Version(version);
315 var packages =
new HashSet<string>();
316 foreach (var entry
in jar.GetEntries())
317 if (entry.Name.EndsWith(
".class"))
318 if (GetPackageName(entry) is { } pn)
319 if (packages.Add(pn))
320 builder = builder.Package(pn);
325 if (jar.Manifest !=
null && jar.Manifest.MainAttributes.TryGetValue(
"Main-Class", out var mainClass))
328 mainClass = mainClass.Replace(
'/',
'.');
331 if (Identifer.IsClassName(mainClass))
333 var pn = GetPackageName(mainClass);
334 if (pn is not
null && packages.Contains(pn))
335 builder = builder.MainClass(mainClass);
339 return builder.Build();
349 string? GetPackageName(JarFileEntry entry)
351 return GetPackageName(entry.Name);
361 string? GetPackageName(
string className)
363 if (className.EndsWith(
'/'))
364 throw new InvalidOperationException();
366 int index = className.LastIndexOf(
'/');
369 if (className.EndsWith(
".class") && className != MODULE_INFO)
370 throw new InvalidModuleDescriptorException($
"{className} found in top-level directory (unnamed package not allowed in module)");
375 var pn = className.Substring(0, index).Replace(
'/',
'.');
376 return Identifer.IsPackageName(pn) ? pn :
null;
384 static void GetModuleNameAndVersionFromFileName(
string fileName, out
string name, out ModuleVersion version)
386 if (fileName is
null)
387 throw new ArgumentNullException(nameof(fileName));
390 fileName = Path.GetFileNameWithoutExtension(fileName);
391 var span = (Span<char>)stackalloc
char[fileName.Length];
392 fileName.AsSpan().CopyTo(span);
395 if (Regex.Match(fileName,
@"-(\d+(\.|$))") is Match m and { Success: true })
397 version = TryParseVersion(span.Slice(m.Index + 1));
398 span = span.Slice(0, m.Index);
406 for (var i = 0; i < span.Length; i++)
407 if (
char.IsLetterOrDigit(span[i]) ==
false)
411 fileName = span.ToString();
412 while (fileName.IndexOf(
"..") is
int i && i != -1)
413 fileName = fileName.Remove(i, 1);
416 name = fileName.Trim(
'.');
424 static ModuleVersion TryParseVersion(ReadOnlySpan<char> span)
426 return ModuleVersion.TryParse(span, out var v) ? v :
default;