IKVM11  11
Java SE 11 Virtual Machine for .NET
Loading...
Searching...
No Matches
ModulePath.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Collections.Immutable;
4using System.IO;
5using System.Text.RegularExpressions;
6
7using IKVM.ByteCode.Decoding;
9
11{
12
20 internal class ModulePath : ModuleFinder
21 {
22
29 public new static IModuleFinder Create(ImmutableArray<string> entries)
30 {
31 if (entries.IsDefault)
32 throw new ArgumentNullException(nameof(entries));
33
34 return new ModulePath(JarFile.RUNTIME_VERSION, entries);
35 }
36
44 public static IModuleFinder Create(RuntimeVersion version, ImmutableArray<string> entries)
45 {
46 return new ModulePath(version, entries);
47 }
48
49 const string MODULE_INFO = "module-info.class";
50 const string AUTOMATIC_MODULE_NAME = "Automatic-Module-Name";
51
52 readonly RuntimeVersion _runtimeVersion;
53 readonly ImmutableArray<string> _entries;
54 readonly Dictionary<string, ModuleReference> _cachedModules = new();
55
56 ImmutableHashSet<ModuleReference>? _all;
57 int _next;
58
64 public ModulePath(RuntimeVersion releaseVersion, ImmutableArray<string> entries)
65 {
66 if (entries.IsDefault)
67 throw new ArgumentException("Uninitialized immutable array.", nameof(entries));
68
69 _runtimeVersion = releaseVersion;
70 _entries = entries;
71 }
72
74 public override ModuleReference? Find(string name)
75 {
76 if (name is null)
77 throw new ArgumentNullException(nameof(name));
78
79 var m = _cachedModules.GetValueOrDefault(name);
80 if (m is not null)
81 return m;
82
83 while (HasNextEntry())
84 {
85 ScanNextEntry();
86 m = _cachedModules.GetValueOrDefault(name);
87 if (m is not null)
88 return m;
89 }
90
91 return null;
92 }
93
95 public override ImmutableHashSet<ModuleReference> FindAll()
96 {
97 if (_all is null)
98 {
99 ScanAll();
100 _all = [.. _cachedModules.Values];
101 }
102
103 return _all;
104 }
105
109 void ScanAll()
110 {
111 while (HasNextEntry())
112 ScanNextEntry();
113 }
114
119 bool HasNextEntry()
120 {
121 return _next < _entries.Length;
122 }
123
127 void ScanNextEntry()
128 {
129 if (HasNextEntry())
130 {
131 var modules = Scan(_entries[_next]);
132 _next++;
133
134 // update cache, ignore duplicates
135 foreach (var kvp in modules)
136 _cachedModules.TryAdd(kvp.Key, kvp.Value);
137 }
138 }
139
146 Dictionary<string, ModuleReference> Scan(string entry)
147 {
148 try
149 {
150 if (Directory.Exists(entry))
151 {
152 var mi = Path.Combine(entry, MODULE_INFO);
153 if (File.Exists(mi) == false)
154 return ScanDirectory(entry);
155 }
156
157 var mref = ReadModule(entry);
158 if (mref is not null)
159 return new Dictionary<string, ModuleReference>() { [mref.Descriptor.Name] = mref };
160
161 throw new FindException($"Module format not recognized: {entry}");
162 }
163 catch (IOException e)
164 {
165 throw new FindException(e);
166 }
167 }
168
175 Dictionary<string, ModuleReference> ScanDirectory(string dir)
176 {
177 var nameToReference = new Dictionary<string, ModuleReference>();
178
179 foreach (var entry in Directory.EnumerateFileSystemEntries(dir))
180 {
181 var mref = ReadModule(entry);
182 if (mref is not null)
183 {
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}).");
187
188 nameToReference.Add(name, mref);
189 }
190 }
191
192 return nameToReference;
193 }
194
202 ModuleReference? ReadModule(string path)
203 {
204 if (path is null)
205 throw new ArgumentNullException(nameof(path));
206
207 path = Path.GetFullPath(path);
208
209 try
210 {
211 // entry is a directory
212 if (Directory.Exists(path))
213 return ReadExplodedModule(path);
214
215 // entry is a JAR file
216 if (File.Exists(path))
217 if (Path.GetExtension(path) == ".jar")
218 return ReadJar(path);
219
220 return null;
221 }
222 catch (InvalidModuleDescriptorException e)
223 {
224 throw new FindException($"Error reading module: {path}", e);
225 }
226 }
227
233 ModuleReference? ReadExplodedModule(string dir)
234 {
235 if (dir is null)
236 throw new ArgumentNullException(nameof(dir));
237
238 dir = Path.GetFullPath(dir);
239 var moduleInfoFile = new FileInfo(Path.Combine(dir, MODULE_INFO));
240 if (moduleInfoFile.Exists == false)
241 return null;
242
243 using var desc = moduleInfoFile.Open(FileMode.Open);
244 using var clzz = ClassFile.Read(desc);
245 return ModuleReference.CreateExplodedModule(ModuleDescriptor.Read(clzz), _runtimeVersion, dir);
246 }
247
253 ModuleReference ReadJar(string file)
254 {
255 using var jar = new JarFile(file, _runtimeVersion);
256
257 var moduleInfoEntry = jar.GetEntry(MODULE_INFO);
258 if (moduleInfoEntry is null)
259 {
260 try
261 {
262 return ModuleReference.CreateJarModule(DeriveModuleDescriptor(jar, file), _runtimeVersion, file);
263 }
264 catch (Exception e)
265 {
266 throw new FindException($"Unable to derive module descriptor for '{file}'.", e);
267 }
268 }
269 else
270 {
271 using var desc = moduleInfoEntry.Value.Open();
272 using var clzz = ClassFile.Read(desc);
273 return ModuleReference.CreateJarModule(ModuleDescriptor.Read(clzz), _runtimeVersion, file);
274 }
275 }
276
290 ModuleDescriptor DeriveModuleDescriptor(JarFile jar, string file)
291 {
292 string? moduleName = null;
293
294 // attempt to find automatic module name from manifest
295 var man = jar.Manifest;
296 if (man is not null && man.MainAttributes.TryGetValue(AUTOMATIC_MODULE_NAME, out var amn))
297 moduleName = amn;
298
299 // derive from file name
300 GetModuleNameAndVersionFromFileName(file, out var fmn, out var version);
301 if (moduleName is null && fmn is not null)
302 moduleName = fmn;
303
304 // at this point we should have a module name
305 if (moduleName is null)
306 throw new FindException($"Could not derive module name from '{file}'.");
307
308 // build new automatic module
309 var builder = ModuleDescriptor.CreateAutomaticModule(moduleName);
310 if (version.IsValid)
311 builder = builder.Version(version);
312
313
314 // derive packages from class files
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);
321
322 // TODO service names
323
324 // Main-Class attribute if it exists
325 if (jar.Manifest != null && jar.Manifest.MainAttributes.TryGetValue("Main-Class", out var mainClass))
326 {
327 // may be stored in binary format
328 mainClass = mainClass.Replace('/', '.');
329
330 // check for valid class name
331 if (Identifer.IsClassName(mainClass))
332 {
333 var pn = GetPackageName(mainClass);
334 if (pn is not null && packages.Contains(pn))
335 builder = builder.MainClass(mainClass);
336 }
337 }
338
339 return builder.Build();
340 }
341
349 string? GetPackageName(JarFileEntry entry)
350 {
351 return GetPackageName(entry.Name);
352 }
353
361 string? GetPackageName(string className)
362 {
363 if (className.EndsWith('/'))
364 throw new InvalidOperationException();
365
366 int index = className.LastIndexOf('/');
367 if (index == -1)
368 {
369 if (className.EndsWith(".class") && className != MODULE_INFO)
370 throw new InvalidModuleDescriptorException($"{className} found in top-level directory (unnamed package not allowed in module)");
371
372 return null;
373 }
374
375 var pn = className.Substring(0, index).Replace('/', '.');
376 return Identifer.IsPackageName(pn) ? pn : null;
377 }
378
384 static void GetModuleNameAndVersionFromFileName(string fileName, out string name, out ModuleVersion version)
385 {
386 if (fileName is null)
387 throw new ArgumentNullException(nameof(fileName));
388
389 // The ".jar" suffix is removed.
390 fileName = Path.GetFileNameWithoutExtension(fileName);
391 var span = (Span<char>)stackalloc char[fileName.Length];
392 fileName.AsSpan().CopyTo(span);
393
394 // If the name matches the regular expression "-(\\d+(\\.|$))" then the module name will be derived from the subsequence preceding the hyphen of the first occurrence.
395 if (Regex.Match(fileName, @"-(\d+(\.|$))") is Match m and { Success: true })
396 {
397 version = TryParseVersion(span.Slice(m.Index + 1));
398 span = span.Slice(0, m.Index);
399 }
400 else
401 {
402 version = default;
403 }
404
405 // All non-alphanumeric characters ([^A-Za-z0-9]) in the module name are replaced with a dot ("."),
406 for (var i = 0; i < span.Length; i++)
407 if (char.IsLetterOrDigit(span[i]) == false)
408 span[i] = '.';
409
410 // all repeating dots are replaced with one dot,
411 fileName = span.ToString();
412 while (fileName.IndexOf("..") is int i && i != -1)
413 fileName = fileName.Remove(i, 1);
414
415 // and all leading and trailing dots are removed.
416 name = fileName.Trim('.');
417 }
418
424 static ModuleVersion TryParseVersion(ReadOnlySpan<char> span)
425 {
426 return ModuleVersion.TryParse(span, out var v) ? v : default;
427 }
428
429 }
430
431}