up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,13 +1,13 @@
global using System;
global using System.Collections.Generic;
global using System.Globalization;
global using System.IO;
global using System.IO.Compression;
global using System.Linq;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.RegularExpressions;
global using System.Threading;
global using System.Threading.Tasks;
global using StellaOps.Scanner.Analyzers.Lang;
global using System;
global using System.Collections.Generic;
global using System.Globalization;
global using System.IO;
global using System.IO.Compression;
global using System.Linq;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.RegularExpressions;
global using System.Threading;
global using System.Threading.Tasks;
global using StellaOps.Scanner.Analyzers.Lang;

View File

@@ -1,62 +1,62 @@
using System.IO.Compression;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal enum JavaClassLocationKind
{
ArchiveEntry,
EmbeddedArchiveEntry,
}
internal sealed record JavaClassLocation(
JavaClassLocationKind Kind,
JavaArchive Archive,
JavaArchiveEntry Entry,
string? NestedClassPath)
{
public static JavaClassLocation ForArchive(JavaArchive archive, JavaArchiveEntry entry)
=> new(JavaClassLocationKind.ArchiveEntry, archive, entry, NestedClassPath: null);
public static JavaClassLocation ForEmbedded(JavaArchive archive, JavaArchiveEntry entry, string nestedClassPath)
=> new(JavaClassLocationKind.EmbeddedArchiveEntry, archive, entry, nestedClassPath);
public Stream OpenClassStream(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Kind switch
{
JavaClassLocationKind.ArchiveEntry => Archive.OpenEntry(Entry),
JavaClassLocationKind.EmbeddedArchiveEntry => OpenEmbeddedEntryStream(cancellationToken),
_ => throw new InvalidOperationException($"Unsupported class location kind '{Kind}'."),
};
}
private Stream OpenEmbeddedEntryStream(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var embeddedStream = Archive.OpenEntry(Entry);
using var buffer = new MemoryStream();
embeddedStream.CopyTo(buffer);
buffer.Position = 0;
using var nestedArchive = new ZipArchive(buffer, ZipArchiveMode.Read, leaveOpen: true);
if (NestedClassPath is null)
{
throw new InvalidOperationException($"Nested class path not specified for embedded entry '{Entry.OriginalPath}'.");
}
var classEntry = nestedArchive.GetEntry(NestedClassPath);
if (classEntry is null)
{
throw new FileNotFoundException($"Class '{NestedClassPath}' not found inside embedded archive entry '{Entry.OriginalPath}'.");
}
using var classStream = classEntry.Open();
var output = new MemoryStream();
classStream.CopyTo(output);
output.Position = 0;
return output;
}
}
using System.IO.Compression;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal enum JavaClassLocationKind
{
ArchiveEntry,
EmbeddedArchiveEntry,
}
internal sealed record JavaClassLocation(
JavaClassLocationKind Kind,
JavaArchive Archive,
JavaArchiveEntry Entry,
string? NestedClassPath)
{
public static JavaClassLocation ForArchive(JavaArchive archive, JavaArchiveEntry entry)
=> new(JavaClassLocationKind.ArchiveEntry, archive, entry, NestedClassPath: null);
public static JavaClassLocation ForEmbedded(JavaArchive archive, JavaArchiveEntry entry, string nestedClassPath)
=> new(JavaClassLocationKind.EmbeddedArchiveEntry, archive, entry, nestedClassPath);
public Stream OpenClassStream(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Kind switch
{
JavaClassLocationKind.ArchiveEntry => Archive.OpenEntry(Entry),
JavaClassLocationKind.EmbeddedArchiveEntry => OpenEmbeddedEntryStream(cancellationToken),
_ => throw new InvalidOperationException($"Unsupported class location kind '{Kind}'."),
};
}
private Stream OpenEmbeddedEntryStream(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var embeddedStream = Archive.OpenEntry(Entry);
using var buffer = new MemoryStream();
embeddedStream.CopyTo(buffer);
buffer.Position = 0;
using var nestedArchive = new ZipArchive(buffer, ZipArchiveMode.Read, leaveOpen: true);
if (NestedClassPath is null)
{
throw new InvalidOperationException($"Nested class path not specified for embedded entry '{Entry.OriginalPath}'.");
}
var classEntry = nestedArchive.GetEntry(NestedClassPath);
if (classEntry is null)
{
throw new FileNotFoundException($"Class '{NestedClassPath}' not found inside embedded archive entry '{Entry.OriginalPath}'.");
}
using var classStream = classEntry.Open();
var output = new MemoryStream();
classStream.CopyTo(output);
output.Position = 0;
return output;
}
}

View File

@@ -1,102 +1,102 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal sealed class JavaClassPathAnalysis
{
public JavaClassPathAnalysis(
IEnumerable<JavaClassPathSegment> segments,
IEnumerable<JavaModuleDescriptor> modules,
IEnumerable<JavaClassDuplicate> duplicateClasses,
IEnumerable<JavaSplitPackage> splitPackages)
{
Segments = segments
.Where(static segment => segment is not null)
.OrderBy(static segment => segment.Order)
.ThenBy(static segment => segment.Identifier, StringComparer.Ordinal)
.ToImmutableArray();
Modules = modules
.Where(static module => module is not null)
.OrderBy(static module => module.Name, StringComparer.Ordinal)
.ThenBy(static module => module.Source, StringComparer.Ordinal)
.ToImmutableArray();
DuplicateClasses = duplicateClasses
.Where(static duplicate => duplicate is not null)
.OrderBy(static duplicate => duplicate.ClassName, StringComparer.Ordinal)
.ToImmutableArray();
SplitPackages = splitPackages
.Where(static split => split is not null)
.OrderBy(static split => split.PackageName, StringComparer.Ordinal)
.ToImmutableArray();
}
public ImmutableArray<JavaClassPathSegment> Segments { get; }
public ImmutableArray<JavaModuleDescriptor> Modules { get; }
public ImmutableArray<JavaClassDuplicate> DuplicateClasses { get; }
public ImmutableArray<JavaSplitPackage> SplitPackages { get; }
}
internal sealed class JavaClassPathSegment
{
public JavaClassPathSegment(
string identifier,
string displayPath,
JavaClassPathSegmentKind kind,
JavaPackagingKind packaging,
int order,
JavaModuleDescriptor? module,
ImmutableSortedSet<string> classes,
ImmutableDictionary<string, JavaPackageFingerprint> packages,
ImmutableDictionary<string, ImmutableArray<string>> serviceDefinitions,
ImmutableDictionary<string, JavaClassLocation> classLocations)
{
Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier));
DisplayPath = displayPath ?? throw new ArgumentNullException(nameof(displayPath));
Kind = kind;
Packaging = packaging;
Order = order;
Module = module;
Classes = classes;
Packages = packages ?? ImmutableDictionary<string, JavaPackageFingerprint>.Empty;
ServiceDefinitions = serviceDefinitions ?? ImmutableDictionary<string, ImmutableArray<string>>.Empty;
ClassLocations = classLocations ?? ImmutableDictionary<string, JavaClassLocation>.Empty;
}
public string Identifier { get; }
public string DisplayPath { get; }
public JavaClassPathSegmentKind Kind { get; }
public JavaPackagingKind Packaging { get; }
public int Order { get; }
public JavaModuleDescriptor? Module { get; }
public ImmutableSortedSet<string> Classes { get; }
public ImmutableDictionary<string, JavaPackageFingerprint> Packages { get; }
public ImmutableDictionary<string, ImmutableArray<string>> ServiceDefinitions { get; }
public ImmutableDictionary<string, JavaClassLocation> ClassLocations { get; }
}
internal enum JavaClassPathSegmentKind
{
Archive,
Directory,
}
internal sealed record JavaPackageFingerprint(string PackageName, int ClassCount, string Fingerprint);
internal sealed record JavaClassDuplicate(string ClassName, ImmutableArray<string> SegmentIdentifiers);
internal sealed record JavaSplitPackage(string PackageName, ImmutableArray<string> SegmentIdentifiers);
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal sealed class JavaClassPathAnalysis
{
public JavaClassPathAnalysis(
IEnumerable<JavaClassPathSegment> segments,
IEnumerable<JavaModuleDescriptor> modules,
IEnumerable<JavaClassDuplicate> duplicateClasses,
IEnumerable<JavaSplitPackage> splitPackages)
{
Segments = segments
.Where(static segment => segment is not null)
.OrderBy(static segment => segment.Order)
.ThenBy(static segment => segment.Identifier, StringComparer.Ordinal)
.ToImmutableArray();
Modules = modules
.Where(static module => module is not null)
.OrderBy(static module => module.Name, StringComparer.Ordinal)
.ThenBy(static module => module.Source, StringComparer.Ordinal)
.ToImmutableArray();
DuplicateClasses = duplicateClasses
.Where(static duplicate => duplicate is not null)
.OrderBy(static duplicate => duplicate.ClassName, StringComparer.Ordinal)
.ToImmutableArray();
SplitPackages = splitPackages
.Where(static split => split is not null)
.OrderBy(static split => split.PackageName, StringComparer.Ordinal)
.ToImmutableArray();
}
public ImmutableArray<JavaClassPathSegment> Segments { get; }
public ImmutableArray<JavaModuleDescriptor> Modules { get; }
public ImmutableArray<JavaClassDuplicate> DuplicateClasses { get; }
public ImmutableArray<JavaSplitPackage> SplitPackages { get; }
}
internal sealed class JavaClassPathSegment
{
public JavaClassPathSegment(
string identifier,
string displayPath,
JavaClassPathSegmentKind kind,
JavaPackagingKind packaging,
int order,
JavaModuleDescriptor? module,
ImmutableSortedSet<string> classes,
ImmutableDictionary<string, JavaPackageFingerprint> packages,
ImmutableDictionary<string, ImmutableArray<string>> serviceDefinitions,
ImmutableDictionary<string, JavaClassLocation> classLocations)
{
Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier));
DisplayPath = displayPath ?? throw new ArgumentNullException(nameof(displayPath));
Kind = kind;
Packaging = packaging;
Order = order;
Module = module;
Classes = classes;
Packages = packages ?? ImmutableDictionary<string, JavaPackageFingerprint>.Empty;
ServiceDefinitions = serviceDefinitions ?? ImmutableDictionary<string, ImmutableArray<string>>.Empty;
ClassLocations = classLocations ?? ImmutableDictionary<string, JavaClassLocation>.Empty;
}
public string Identifier { get; }
public string DisplayPath { get; }
public JavaClassPathSegmentKind Kind { get; }
public JavaPackagingKind Packaging { get; }
public int Order { get; }
public JavaModuleDescriptor? Module { get; }
public ImmutableSortedSet<string> Classes { get; }
public ImmutableDictionary<string, JavaPackageFingerprint> Packages { get; }
public ImmutableDictionary<string, ImmutableArray<string>> ServiceDefinitions { get; }
public ImmutableDictionary<string, JavaClassLocation> ClassLocations { get; }
}
internal enum JavaClassPathSegmentKind
{
Archive,
Directory,
}
internal sealed record JavaPackageFingerprint(string PackageName, int ClassCount, string Fingerprint);
internal sealed record JavaClassDuplicate(string ClassName, ImmutableArray<string> SegmentIdentifiers);
internal sealed record JavaSplitPackage(string PackageName, ImmutableArray<string> SegmentIdentifiers);

View File

@@ -1,22 +1,22 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal sealed record JavaModuleDescriptor(
string Name,
string? Version,
ushort Flags,
ImmutableArray<JavaModuleRequires> Requires,
ImmutableArray<JavaModuleExports> Exports,
ImmutableArray<JavaModuleOpens> Opens,
ImmutableArray<string> Uses,
ImmutableArray<JavaModuleProvides> Provides,
string Source);
internal sealed record JavaModuleRequires(string Name, ushort Flags, string? Version);
internal sealed record JavaModuleExports(string Package, ushort Flags, ImmutableArray<string> Targets);
internal sealed record JavaModuleOpens(string Package, ushort Flags, ImmutableArray<string> Targets);
internal sealed record JavaModuleProvides(string Service, ImmutableArray<string> Implementations);
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal sealed record JavaModuleDescriptor(
string Name,
string? Version,
ushort Flags,
ImmutableArray<JavaModuleRequires> Requires,
ImmutableArray<JavaModuleExports> Exports,
ImmutableArray<JavaModuleOpens> Opens,
ImmutableArray<string> Uses,
ImmutableArray<JavaModuleProvides> Provides,
string Source);
internal sealed record JavaModuleRequires(string Name, ushort Flags, string? Version);
internal sealed record JavaModuleExports(string Package, ushort Flags, ImmutableArray<string> Targets);
internal sealed record JavaModuleOpens(string Package, ushort Flags, ImmutableArray<string> Targets);
internal sealed record JavaModuleProvides(string Service, ImmutableArray<string> Implementations);

View File

@@ -1,367 +1,367 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal static class JavaModuleInfoParser
{
public static JavaModuleDescriptor? TryParse(Stream stream, string sourceIdentifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(stream);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (!TryReadMagic(reader, out var magic) || magic != 0xCAFEBABE)
{
return null;
}
// Skip minor/major version.
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
var constantPool = ReadConstantPool(reader);
cancellationToken.ThrowIfCancellationRequested();
// access_flags, this_class, super_class
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
// interfaces
var interfacesCount = ReadUInt16(reader);
SkipBytes(reader, interfacesCount * 2);
// fields
var fieldsCount = ReadUInt16(reader);
SkipMembers(reader, fieldsCount);
// methods
var methodsCount = ReadUInt16(reader);
SkipMembers(reader, methodsCount);
var attributesCount = ReadUInt16(reader);
JavaModuleDescriptor? descriptor = null;
for (var i = 0; i < attributesCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var nameIndex = ReadUInt16(reader);
var length = ReadUInt32(reader);
var attributeName = GetUtf8(constantPool, nameIndex);
if (string.Equals(attributeName, "Module", StringComparison.Ordinal))
{
descriptor = ParseModuleAttribute(reader, constantPool, sourceIdentifier);
}
else
{
SkipBytes(reader, (int)length);
}
}
return descriptor;
}
private static JavaModuleDescriptor ParseModuleAttribute(BinaryReader reader, ConstantPoolEntry[] constantPool, string sourceIdentifier)
{
var moduleNameIndex = ReadUInt16(reader);
var moduleFlags = ReadUInt16(reader);
var moduleVersionIndex = ReadUInt16(reader);
var moduleName = GetModuleName(constantPool, moduleNameIndex);
var moduleVersion = moduleVersionIndex != 0 ? GetUtf8(constantPool, moduleVersionIndex) : null;
var requiresCount = ReadUInt16(reader);
var requiresBuilder = ImmutableArray.CreateBuilder<JavaModuleRequires>(requiresCount);
for (var i = 0; i < requiresCount; i++)
{
var requiresIndex = ReadUInt16(reader);
var requiresFlags = ReadUInt16(reader);
var requiresVersionIndex = ReadUInt16(reader);
var requiresName = GetModuleName(constantPool, requiresIndex);
var requiresVersion = requiresVersionIndex != 0 ? GetUtf8(constantPool, requiresVersionIndex) : null;
requiresBuilder.Add(new JavaModuleRequires(requiresName, requiresFlags, requiresVersion));
}
var exportsCount = ReadUInt16(reader);
var exportsBuilder = ImmutableArray.CreateBuilder<JavaModuleExports>(exportsCount);
for (var i = 0; i < exportsCount; i++)
{
var exportsIndex = ReadUInt16(reader);
var exportsFlags = ReadUInt16(reader);
var exportsToCount = ReadUInt16(reader);
var targetsBuilder = ImmutableArray.CreateBuilder<string>(exportsToCount);
for (var j = 0; j < exportsToCount; j++)
{
var targetIndex = ReadUInt16(reader);
targetsBuilder.Add(GetModuleName(constantPool, targetIndex));
}
var packageName = GetPackageName(constantPool, exportsIndex);
exportsBuilder.Add(new JavaModuleExports(packageName, exportsFlags, targetsBuilder.ToImmutable()));
}
var opensCount = ReadUInt16(reader);
var opensBuilder = ImmutableArray.CreateBuilder<JavaModuleOpens>(opensCount);
for (var i = 0; i < opensCount; i++)
{
var opensIndex = ReadUInt16(reader);
var opensFlags = ReadUInt16(reader);
var opensToCount = ReadUInt16(reader);
var targetsBuilder = ImmutableArray.CreateBuilder<string>(opensToCount);
for (var j = 0; j < opensToCount; j++)
{
var targetIndex = ReadUInt16(reader);
targetsBuilder.Add(GetModuleName(constantPool, targetIndex));
}
var packageName = GetPackageName(constantPool, opensIndex);
opensBuilder.Add(new JavaModuleOpens(packageName, opensFlags, targetsBuilder.ToImmutable()));
}
var usesCount = ReadUInt16(reader);
var usesBuilder = ImmutableArray.CreateBuilder<string>(usesCount);
for (var i = 0; i < usesCount; i++)
{
var classIndex = ReadUInt16(reader);
usesBuilder.Add(GetClassName(constantPool, classIndex));
}
var providesCount = ReadUInt16(reader);
var providesBuilder = ImmutableArray.CreateBuilder<JavaModuleProvides>(providesCount);
for (var i = 0; i < providesCount; i++)
{
var serviceIndex = ReadUInt16(reader);
var providesWithCount = ReadUInt16(reader);
var implementationsBuilder = ImmutableArray.CreateBuilder<string>(providesWithCount);
for (var j = 0; j < providesWithCount; j++)
{
var implIndex = ReadUInt16(reader);
implementationsBuilder.Add(GetClassName(constantPool, implIndex));
}
var serviceName = GetClassName(constantPool, serviceIndex);
providesBuilder.Add(new JavaModuleProvides(serviceName, implementationsBuilder.ToImmutable()));
}
return new JavaModuleDescriptor(
moduleName,
moduleVersion,
moduleFlags,
requiresBuilder.ToImmutable(),
exportsBuilder.ToImmutable(),
opensBuilder.ToImmutable(),
usesBuilder.ToImmutable(),
providesBuilder.ToImmutable(),
sourceIdentifier);
}
private static ConstantPoolEntry[] ReadConstantPool(BinaryReader reader)
{
var count = ReadUInt16(reader);
var pool = new ConstantPoolEntry[count];
var index = 1;
while (index < count)
{
var tag = reader.ReadByte();
switch (tag)
{
case 1: // Utf8
{
var length = ReadUInt16(reader);
var bytes = reader.ReadBytes(length);
var value = Encoding.UTF8.GetString(bytes);
pool[index] = new Utf8Entry(value);
break;
}
case 7: // Class
{
var nameIndex = ReadUInt16(reader);
pool[index] = new ClassEntry(nameIndex);
break;
}
case 8: // String
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(2, SeekOrigin.Current);
break;
case 3: // Integer
case 4: // Float
case 9: // Fieldref
case 10: // Methodref
case 11: // InterfaceMethodref
case 12: // NameAndType
case 17: // Dynamic
case 18: // InvokeDynamic
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(4, SeekOrigin.Current);
break;
case 5: // Long
case 6: // Double
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(8, SeekOrigin.Current);
index++;
break;
case 15: // MethodHandle
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(3, SeekOrigin.Current);
break;
case 16: // MethodType
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(2, SeekOrigin.Current);
break;
case 19: // Module
{
var nameIndex = ReadUInt16(reader);
pool[index] = new ModuleEntry(nameIndex);
break;
}
case 20: // Package
{
var nameIndex = ReadUInt16(reader);
pool[index] = new PackageEntry(nameIndex);
break;
}
default:
throw new InvalidDataException($"Unsupported constant pool tag {tag}.");
}
index++;
}
return pool;
}
private static string GetUtf8(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is Utf8Entry utf8)
{
return utf8.Value;
}
return string.Empty;
}
private static string GetModuleName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is ModuleEntry module)
{
var utf8 = GetUtf8(pool, module.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string GetPackageName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is PackageEntry package)
{
var utf8 = GetUtf8(pool, package.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string GetClassName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is ClassEntry classEntry)
{
var utf8 = GetUtf8(pool, classEntry.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string NormalizeBinaryName(string value)
=> string.IsNullOrEmpty(value) ? string.Empty : value.Replace('/', '.');
private static bool TryReadMagic(BinaryReader reader, out uint magic)
{
if (reader.BaseStream.Length - reader.BaseStream.Position < 4)
{
magic = 0;
return false;
}
magic = ReadUInt32(reader);
return true;
}
private static void SkipMembers(BinaryReader reader, int count)
{
for (var i = 0; i < count; i++)
{
// access_flags, name_index, descriptor_index
reader.BaseStream.Seek(6, SeekOrigin.Current);
var attributesCount = ReadUInt16(reader);
SkipAttributes(reader, attributesCount);
}
}
private static void SkipAttributes(BinaryReader reader, int count)
{
for (var i = 0; i < count; i++)
{
reader.BaseStream.Seek(2, SeekOrigin.Current); // name_index
var length = ReadUInt32(reader);
SkipBytes(reader, (int)length);
}
}
private static void SkipBytes(BinaryReader reader, int count)
{
if (count <= 0)
{
return;
}
reader.BaseStream.Seek(count, SeekOrigin.Current);
}
private static ushort ReadUInt16(BinaryReader reader)
=> BinaryPrimitives.ReadUInt16BigEndian(reader.ReadBytes(sizeof(ushort)));
private static uint ReadUInt32(BinaryReader reader)
=> BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(sizeof(uint)));
private abstract record ConstantPoolEntry(byte Tag);
private sealed record Utf8Entry(string Value) : ConstantPoolEntry(1);
private sealed record ClassEntry(ushort NameIndex) : ConstantPoolEntry(7);
private sealed record ModuleEntry(ushort NameIndex) : ConstantPoolEntry(19);
private sealed record PackageEntry(ushort NameIndex) : ConstantPoolEntry(20);
private sealed record SimpleEntry(byte Tag) : ConstantPoolEntry(Tag);
}
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
internal static class JavaModuleInfoParser
{
public static JavaModuleDescriptor? TryParse(Stream stream, string sourceIdentifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(stream);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (!TryReadMagic(reader, out var magic) || magic != 0xCAFEBABE)
{
return null;
}
// Skip minor/major version.
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
var constantPool = ReadConstantPool(reader);
cancellationToken.ThrowIfCancellationRequested();
// access_flags, this_class, super_class
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
_ = ReadUInt16(reader);
// interfaces
var interfacesCount = ReadUInt16(reader);
SkipBytes(reader, interfacesCount * 2);
// fields
var fieldsCount = ReadUInt16(reader);
SkipMembers(reader, fieldsCount);
// methods
var methodsCount = ReadUInt16(reader);
SkipMembers(reader, methodsCount);
var attributesCount = ReadUInt16(reader);
JavaModuleDescriptor? descriptor = null;
for (var i = 0; i < attributesCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var nameIndex = ReadUInt16(reader);
var length = ReadUInt32(reader);
var attributeName = GetUtf8(constantPool, nameIndex);
if (string.Equals(attributeName, "Module", StringComparison.Ordinal))
{
descriptor = ParseModuleAttribute(reader, constantPool, sourceIdentifier);
}
else
{
SkipBytes(reader, (int)length);
}
}
return descriptor;
}
private static JavaModuleDescriptor ParseModuleAttribute(BinaryReader reader, ConstantPoolEntry[] constantPool, string sourceIdentifier)
{
var moduleNameIndex = ReadUInt16(reader);
var moduleFlags = ReadUInt16(reader);
var moduleVersionIndex = ReadUInt16(reader);
var moduleName = GetModuleName(constantPool, moduleNameIndex);
var moduleVersion = moduleVersionIndex != 0 ? GetUtf8(constantPool, moduleVersionIndex) : null;
var requiresCount = ReadUInt16(reader);
var requiresBuilder = ImmutableArray.CreateBuilder<JavaModuleRequires>(requiresCount);
for (var i = 0; i < requiresCount; i++)
{
var requiresIndex = ReadUInt16(reader);
var requiresFlags = ReadUInt16(reader);
var requiresVersionIndex = ReadUInt16(reader);
var requiresName = GetModuleName(constantPool, requiresIndex);
var requiresVersion = requiresVersionIndex != 0 ? GetUtf8(constantPool, requiresVersionIndex) : null;
requiresBuilder.Add(new JavaModuleRequires(requiresName, requiresFlags, requiresVersion));
}
var exportsCount = ReadUInt16(reader);
var exportsBuilder = ImmutableArray.CreateBuilder<JavaModuleExports>(exportsCount);
for (var i = 0; i < exportsCount; i++)
{
var exportsIndex = ReadUInt16(reader);
var exportsFlags = ReadUInt16(reader);
var exportsToCount = ReadUInt16(reader);
var targetsBuilder = ImmutableArray.CreateBuilder<string>(exportsToCount);
for (var j = 0; j < exportsToCount; j++)
{
var targetIndex = ReadUInt16(reader);
targetsBuilder.Add(GetModuleName(constantPool, targetIndex));
}
var packageName = GetPackageName(constantPool, exportsIndex);
exportsBuilder.Add(new JavaModuleExports(packageName, exportsFlags, targetsBuilder.ToImmutable()));
}
var opensCount = ReadUInt16(reader);
var opensBuilder = ImmutableArray.CreateBuilder<JavaModuleOpens>(opensCount);
for (var i = 0; i < opensCount; i++)
{
var opensIndex = ReadUInt16(reader);
var opensFlags = ReadUInt16(reader);
var opensToCount = ReadUInt16(reader);
var targetsBuilder = ImmutableArray.CreateBuilder<string>(opensToCount);
for (var j = 0; j < opensToCount; j++)
{
var targetIndex = ReadUInt16(reader);
targetsBuilder.Add(GetModuleName(constantPool, targetIndex));
}
var packageName = GetPackageName(constantPool, opensIndex);
opensBuilder.Add(new JavaModuleOpens(packageName, opensFlags, targetsBuilder.ToImmutable()));
}
var usesCount = ReadUInt16(reader);
var usesBuilder = ImmutableArray.CreateBuilder<string>(usesCount);
for (var i = 0; i < usesCount; i++)
{
var classIndex = ReadUInt16(reader);
usesBuilder.Add(GetClassName(constantPool, classIndex));
}
var providesCount = ReadUInt16(reader);
var providesBuilder = ImmutableArray.CreateBuilder<JavaModuleProvides>(providesCount);
for (var i = 0; i < providesCount; i++)
{
var serviceIndex = ReadUInt16(reader);
var providesWithCount = ReadUInt16(reader);
var implementationsBuilder = ImmutableArray.CreateBuilder<string>(providesWithCount);
for (var j = 0; j < providesWithCount; j++)
{
var implIndex = ReadUInt16(reader);
implementationsBuilder.Add(GetClassName(constantPool, implIndex));
}
var serviceName = GetClassName(constantPool, serviceIndex);
providesBuilder.Add(new JavaModuleProvides(serviceName, implementationsBuilder.ToImmutable()));
}
return new JavaModuleDescriptor(
moduleName,
moduleVersion,
moduleFlags,
requiresBuilder.ToImmutable(),
exportsBuilder.ToImmutable(),
opensBuilder.ToImmutable(),
usesBuilder.ToImmutable(),
providesBuilder.ToImmutable(),
sourceIdentifier);
}
private static ConstantPoolEntry[] ReadConstantPool(BinaryReader reader)
{
var count = ReadUInt16(reader);
var pool = new ConstantPoolEntry[count];
var index = 1;
while (index < count)
{
var tag = reader.ReadByte();
switch (tag)
{
case 1: // Utf8
{
var length = ReadUInt16(reader);
var bytes = reader.ReadBytes(length);
var value = Encoding.UTF8.GetString(bytes);
pool[index] = new Utf8Entry(value);
break;
}
case 7: // Class
{
var nameIndex = ReadUInt16(reader);
pool[index] = new ClassEntry(nameIndex);
break;
}
case 8: // String
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(2, SeekOrigin.Current);
break;
case 3: // Integer
case 4: // Float
case 9: // Fieldref
case 10: // Methodref
case 11: // InterfaceMethodref
case 12: // NameAndType
case 17: // Dynamic
case 18: // InvokeDynamic
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(4, SeekOrigin.Current);
break;
case 5: // Long
case 6: // Double
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(8, SeekOrigin.Current);
index++;
break;
case 15: // MethodHandle
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(3, SeekOrigin.Current);
break;
case 16: // MethodType
pool[index] = new SimpleEntry(tag);
reader.BaseStream.Seek(2, SeekOrigin.Current);
break;
case 19: // Module
{
var nameIndex = ReadUInt16(reader);
pool[index] = new ModuleEntry(nameIndex);
break;
}
case 20: // Package
{
var nameIndex = ReadUInt16(reader);
pool[index] = new PackageEntry(nameIndex);
break;
}
default:
throw new InvalidDataException($"Unsupported constant pool tag {tag}.");
}
index++;
}
return pool;
}
private static string GetUtf8(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is Utf8Entry utf8)
{
return utf8.Value;
}
return string.Empty;
}
private static string GetModuleName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is ModuleEntry module)
{
var utf8 = GetUtf8(pool, module.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string GetPackageName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is PackageEntry package)
{
var utf8 = GetUtf8(pool, package.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string GetClassName(ConstantPoolEntry[] pool, int index)
{
if (index <= 0 || index >= pool.Length)
{
return string.Empty;
}
if (pool[index] is ClassEntry classEntry)
{
var utf8 = GetUtf8(pool, classEntry.NameIndex);
return NormalizeBinaryName(utf8);
}
return string.Empty;
}
private static string NormalizeBinaryName(string value)
=> string.IsNullOrEmpty(value) ? string.Empty : value.Replace('/', '.');
private static bool TryReadMagic(BinaryReader reader, out uint magic)
{
if (reader.BaseStream.Length - reader.BaseStream.Position < 4)
{
magic = 0;
return false;
}
magic = ReadUInt32(reader);
return true;
}
private static void SkipMembers(BinaryReader reader, int count)
{
for (var i = 0; i < count; i++)
{
// access_flags, name_index, descriptor_index
reader.BaseStream.Seek(6, SeekOrigin.Current);
var attributesCount = ReadUInt16(reader);
SkipAttributes(reader, attributesCount);
}
}
private static void SkipAttributes(BinaryReader reader, int count)
{
for (var i = 0; i < count; i++)
{
reader.BaseStream.Seek(2, SeekOrigin.Current); // name_index
var length = ReadUInt32(reader);
SkipBytes(reader, (int)length);
}
}
private static void SkipBytes(BinaryReader reader, int count)
{
if (count <= 0)
{
return;
}
reader.BaseStream.Seek(count, SeekOrigin.Current);
}
private static ushort ReadUInt16(BinaryReader reader)
=> BinaryPrimitives.ReadUInt16BigEndian(reader.ReadBytes(sizeof(ushort)));
private static uint ReadUInt32(BinaryReader reader)
=> BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(sizeof(uint)));
private abstract record ConstantPoolEntry(byte Tag);
private sealed record Utf8Entry(string Value) : ConstantPoolEntry(1);
private sealed record ClassEntry(ushort NameIndex) : ConstantPoolEntry(7);
private sealed record ModuleEntry(ushort NameIndex) : ConstantPoolEntry(19);
private sealed record PackageEntry(ushort NameIndex) : ConstantPoolEntry(20);
private sealed record SimpleEntry(byte Tag) : ConstantPoolEntry(Tag);
}

View File

@@ -1,264 +1,264 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed class JavaArchive
{
private readonly ImmutableDictionary<string, JavaArchiveEntry> _entryMap;
private JavaArchive(
string absolutePath,
string relativePath,
JavaPackagingKind packaging,
ImmutableArray<string> layeredDirectories,
bool isMultiRelease,
bool hasModuleInfo,
ImmutableArray<JavaArchiveEntry> entries)
{
AbsolutePath = absolutePath ?? throw new ArgumentNullException(nameof(absolutePath));
RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
Packaging = packaging;
LayeredDirectories = layeredDirectories;
IsMultiRelease = isMultiRelease;
HasModuleInfo = hasModuleInfo;
Entries = entries;
_entryMap = entries.ToImmutableDictionary(static entry => entry.EffectivePath, static entry => entry, StringComparer.Ordinal);
}
public string AbsolutePath { get; }
public string RelativePath { get; }
public JavaPackagingKind Packaging { get; }
public ImmutableArray<string> LayeredDirectories { get; }
public bool IsMultiRelease { get; }
public bool HasModuleInfo { get; }
public ImmutableArray<JavaArchiveEntry> Entries { get; }
public static JavaArchive Load(string absolutePath, string relativePath)
{
ArgumentException.ThrowIfNullOrEmpty(absolutePath);
ArgumentException.ThrowIfNullOrEmpty(relativePath);
using var fileStream = new FileStream(absolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var zip = new ZipArchive(fileStream, ZipArchiveMode.Read, leaveOpen: false);
var layeredDirectories = new HashSet<string>(StringComparer.Ordinal);
var candidates = new Dictionary<string, List<EntryCandidate>>(StringComparer.Ordinal);
var isMultiRelease = false;
var hasModuleInfo = false;
var hasBootInf = false;
var hasWebInf = false;
foreach (var entry in zip.Entries)
{
var normalized = JavaZipEntryUtilities.NormalizeEntryName(entry.FullName);
if (string.IsNullOrEmpty(normalized) || normalized.EndsWith('/'))
{
continue;
}
if (normalized.StartsWith("BOOT-INF/", StringComparison.OrdinalIgnoreCase))
{
hasBootInf = true;
layeredDirectories.Add("BOOT-INF");
}
if (normalized.StartsWith("WEB-INF/", StringComparison.OrdinalIgnoreCase))
{
hasWebInf = true;
layeredDirectories.Add("WEB-INF");
}
var version = 0;
var effectivePath = normalized;
if (JavaZipEntryUtilities.TryParseMultiReleasePath(normalized, out var candidatePath, out var candidateVersion))
{
effectivePath = candidatePath;
version = candidateVersion;
isMultiRelease = true;
}
if (string.IsNullOrEmpty(effectivePath))
{
continue;
}
if (string.Equals(effectivePath, "module-info.class", StringComparison.Ordinal))
{
hasModuleInfo = true;
}
var candidate = new EntryCandidate(
effectivePath,
entry.FullName,
version,
entry.Length,
entry.LastWriteTime.ToUniversalTime());
if (!candidates.TryGetValue(effectivePath, out var bucket))
{
bucket = new List<EntryCandidate>();
candidates[effectivePath] = bucket;
}
bucket.Add(candidate);
}
var entries = new List<JavaArchiveEntry>(candidates.Count);
foreach (var pair in candidates)
{
var selected = pair.Value
.OrderByDescending(static candidate => candidate.Version)
.ThenBy(static candidate => candidate.OriginalPath, StringComparer.Ordinal)
.First();
entries.Add(new JavaArchiveEntry(
pair.Key,
selected.OriginalPath,
selected.Version,
selected.Length,
selected.LastWriteTime));
}
entries.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.EffectivePath, right.EffectivePath));
var packaging = DeterminePackaging(absolutePath, hasBootInf, hasWebInf);
return new JavaArchive(
absolutePath,
relativePath,
packaging,
layeredDirectories
.OrderBy(static directory => directory, StringComparer.Ordinal)
.ToImmutableArray(),
isMultiRelease,
hasModuleInfo,
entries.ToImmutableArray());
}
public bool TryGetEntry(string effectivePath, out JavaArchiveEntry entry)
{
ArgumentNullException.ThrowIfNull(effectivePath);
return _entryMap.TryGetValue(effectivePath, out entry!);
}
public Stream OpenEntry(JavaArchiveEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
var fileStream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
ZipArchive? archive = null;
Stream? entryStream = null;
try
{
archive = new ZipArchive(fileStream, ZipArchiveMode.Read, leaveOpen: false);
var zipEntry = archive.GetEntry(entry.OriginalPath);
if (zipEntry is null)
{
throw new FileNotFoundException($"Entry '{entry.OriginalPath}' not found in archive '{AbsolutePath}'.");
}
entryStream = zipEntry.Open();
return new ZipEntryStream(fileStream, archive, entryStream);
}
catch
{
entryStream?.Dispose();
archive?.Dispose();
fileStream.Dispose();
throw;
}
}
private static JavaPackagingKind DeterminePackaging(string absolutePath, bool hasBootInf, bool hasWebInf)
{
var extension = Path.GetExtension(absolutePath);
return extension switch
{
".war" => JavaPackagingKind.War,
".ear" => JavaPackagingKind.Ear,
".jmod" => JavaPackagingKind.JMod,
".jimage" => JavaPackagingKind.JImage,
".jar" => hasBootInf ? JavaPackagingKind.SpringBootFatJar : JavaPackagingKind.Jar,
_ => JavaPackagingKind.Unknown,
};
}
private sealed record EntryCandidate(
string EffectivePath,
string OriginalPath,
int Version,
long Length,
DateTimeOffset LastWriteTime);
private sealed class ZipEntryStream : Stream
{
private readonly Stream _fileStream;
private readonly ZipArchive _archive;
private readonly Stream _entryStream;
public ZipEntryStream(Stream fileStream, ZipArchive archive, Stream entryStream)
{
_fileStream = fileStream;
_archive = archive;
_entryStream = entryStream;
}
public override bool CanRead => _entryStream.CanRead;
public override bool CanSeek => _entryStream.CanSeek;
public override bool CanWrite => _entryStream.CanWrite;
public override long Length => _entryStream.Length;
public override long Position
{
get => _entryStream.Position;
set => _entryStream.Position = value;
}
public override void Flush() => _entryStream.Flush();
public override int Read(byte[] buffer, int offset, int count)
=> _entryStream.Read(buffer, offset, count);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _entryStream.ReadAsync(buffer, cancellationToken);
public override long Seek(long offset, SeekOrigin origin)
=> _entryStream.Seek(offset, origin);
public override void SetLength(long value)
=> _entryStream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count)
=> _entryStream.Write(buffer, offset, count);
public override ValueTask DisposeAsync()
{
_entryStream.Dispose();
_archive.Dispose();
_fileStream.Dispose();
return ValueTask.CompletedTask;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_entryStream.Dispose();
_archive.Dispose();
_fileStream.Dispose();
}
base.Dispose(disposing);
}
}
}
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed class JavaArchive
{
private readonly ImmutableDictionary<string, JavaArchiveEntry> _entryMap;
private JavaArchive(
string absolutePath,
string relativePath,
JavaPackagingKind packaging,
ImmutableArray<string> layeredDirectories,
bool isMultiRelease,
bool hasModuleInfo,
ImmutableArray<JavaArchiveEntry> entries)
{
AbsolutePath = absolutePath ?? throw new ArgumentNullException(nameof(absolutePath));
RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
Packaging = packaging;
LayeredDirectories = layeredDirectories;
IsMultiRelease = isMultiRelease;
HasModuleInfo = hasModuleInfo;
Entries = entries;
_entryMap = entries.ToImmutableDictionary(static entry => entry.EffectivePath, static entry => entry, StringComparer.Ordinal);
}
public string AbsolutePath { get; }
public string RelativePath { get; }
public JavaPackagingKind Packaging { get; }
public ImmutableArray<string> LayeredDirectories { get; }
public bool IsMultiRelease { get; }
public bool HasModuleInfo { get; }
public ImmutableArray<JavaArchiveEntry> Entries { get; }
public static JavaArchive Load(string absolutePath, string relativePath)
{
ArgumentException.ThrowIfNullOrEmpty(absolutePath);
ArgumentException.ThrowIfNullOrEmpty(relativePath);
using var fileStream = new FileStream(absolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var zip = new ZipArchive(fileStream, ZipArchiveMode.Read, leaveOpen: false);
var layeredDirectories = new HashSet<string>(StringComparer.Ordinal);
var candidates = new Dictionary<string, List<EntryCandidate>>(StringComparer.Ordinal);
var isMultiRelease = false;
var hasModuleInfo = false;
var hasBootInf = false;
var hasWebInf = false;
foreach (var entry in zip.Entries)
{
var normalized = JavaZipEntryUtilities.NormalizeEntryName(entry.FullName);
if (string.IsNullOrEmpty(normalized) || normalized.EndsWith('/'))
{
continue;
}
if (normalized.StartsWith("BOOT-INF/", StringComparison.OrdinalIgnoreCase))
{
hasBootInf = true;
layeredDirectories.Add("BOOT-INF");
}
if (normalized.StartsWith("WEB-INF/", StringComparison.OrdinalIgnoreCase))
{
hasWebInf = true;
layeredDirectories.Add("WEB-INF");
}
var version = 0;
var effectivePath = normalized;
if (JavaZipEntryUtilities.TryParseMultiReleasePath(normalized, out var candidatePath, out var candidateVersion))
{
effectivePath = candidatePath;
version = candidateVersion;
isMultiRelease = true;
}
if (string.IsNullOrEmpty(effectivePath))
{
continue;
}
if (string.Equals(effectivePath, "module-info.class", StringComparison.Ordinal))
{
hasModuleInfo = true;
}
var candidate = new EntryCandidate(
effectivePath,
entry.FullName,
version,
entry.Length,
entry.LastWriteTime.ToUniversalTime());
if (!candidates.TryGetValue(effectivePath, out var bucket))
{
bucket = new List<EntryCandidate>();
candidates[effectivePath] = bucket;
}
bucket.Add(candidate);
}
var entries = new List<JavaArchiveEntry>(candidates.Count);
foreach (var pair in candidates)
{
var selected = pair.Value
.OrderByDescending(static candidate => candidate.Version)
.ThenBy(static candidate => candidate.OriginalPath, StringComparer.Ordinal)
.First();
entries.Add(new JavaArchiveEntry(
pair.Key,
selected.OriginalPath,
selected.Version,
selected.Length,
selected.LastWriteTime));
}
entries.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.EffectivePath, right.EffectivePath));
var packaging = DeterminePackaging(absolutePath, hasBootInf, hasWebInf);
return new JavaArchive(
absolutePath,
relativePath,
packaging,
layeredDirectories
.OrderBy(static directory => directory, StringComparer.Ordinal)
.ToImmutableArray(),
isMultiRelease,
hasModuleInfo,
entries.ToImmutableArray());
}
public bool TryGetEntry(string effectivePath, out JavaArchiveEntry entry)
{
ArgumentNullException.ThrowIfNull(effectivePath);
return _entryMap.TryGetValue(effectivePath, out entry!);
}
public Stream OpenEntry(JavaArchiveEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
var fileStream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
ZipArchive? archive = null;
Stream? entryStream = null;
try
{
archive = new ZipArchive(fileStream, ZipArchiveMode.Read, leaveOpen: false);
var zipEntry = archive.GetEntry(entry.OriginalPath);
if (zipEntry is null)
{
throw new FileNotFoundException($"Entry '{entry.OriginalPath}' not found in archive '{AbsolutePath}'.");
}
entryStream = zipEntry.Open();
return new ZipEntryStream(fileStream, archive, entryStream);
}
catch
{
entryStream?.Dispose();
archive?.Dispose();
fileStream.Dispose();
throw;
}
}
private static JavaPackagingKind DeterminePackaging(string absolutePath, bool hasBootInf, bool hasWebInf)
{
var extension = Path.GetExtension(absolutePath);
return extension switch
{
".war" => JavaPackagingKind.War,
".ear" => JavaPackagingKind.Ear,
".jmod" => JavaPackagingKind.JMod,
".jimage" => JavaPackagingKind.JImage,
".jar" => hasBootInf ? JavaPackagingKind.SpringBootFatJar : JavaPackagingKind.Jar,
_ => JavaPackagingKind.Unknown,
};
}
private sealed record EntryCandidate(
string EffectivePath,
string OriginalPath,
int Version,
long Length,
DateTimeOffset LastWriteTime);
private sealed class ZipEntryStream : Stream
{
private readonly Stream _fileStream;
private readonly ZipArchive _archive;
private readonly Stream _entryStream;
public ZipEntryStream(Stream fileStream, ZipArchive archive, Stream entryStream)
{
_fileStream = fileStream;
_archive = archive;
_entryStream = entryStream;
}
public override bool CanRead => _entryStream.CanRead;
public override bool CanSeek => _entryStream.CanSeek;
public override bool CanWrite => _entryStream.CanWrite;
public override long Length => _entryStream.Length;
public override long Position
{
get => _entryStream.Position;
set => _entryStream.Position = value;
}
public override void Flush() => _entryStream.Flush();
public override int Read(byte[] buffer, int offset, int count)
=> _entryStream.Read(buffer, offset, count);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _entryStream.ReadAsync(buffer, cancellationToken);
public override long Seek(long offset, SeekOrigin origin)
=> _entryStream.Seek(offset, origin);
public override void SetLength(long value)
=> _entryStream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count)
=> _entryStream.Write(buffer, offset, count);
public override ValueTask DisposeAsync()
{
_entryStream.Dispose();
_archive.Dispose();
_fileStream.Dispose();
return ValueTask.CompletedTask;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_entryStream.Dispose();
_archive.Dispose();
_fileStream.Dispose();
}
base.Dispose(disposing);
}
}
}

View File

@@ -1,8 +1,8 @@
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed record JavaArchiveEntry(
string EffectivePath,
string OriginalPath,
int Version,
long Length,
DateTimeOffset LastWriteTime);
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed record JavaArchiveEntry(
string EffectivePath,
string OriginalPath,
int Version,
long Length,
DateTimeOffset LastWriteTime);

View File

@@ -1,12 +1,12 @@
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal enum JavaPackagingKind
{
Jar,
SpringBootFatJar,
War,
Ear,
JMod,
JImage,
Unknown,
}
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal enum JavaPackagingKind
{
Jar,
SpringBootFatJar,
War,
Ear,
JMod,
JImage,
Unknown,
}

View File

@@ -1,68 +1,68 @@
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaReleaseFileParser
{
public static JavaReleaseMetadata Parse(string filePath)
{
ArgumentException.ThrowIfNullOrEmpty(filePath);
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? line;
while ((line = reader.ReadLine()) is not null)
{
line = line.Trim();
if (line.Length == 0 || line.StartsWith('#'))
{
continue;
}
var separatorIndex = line.IndexOf('=');
if (separatorIndex <= 0)
{
continue;
}
var key = line[..separatorIndex].Trim();
if (key.Length == 0)
{
continue;
}
var value = line[(separatorIndex + 1)..].Trim();
map[key] = TrimQuotes(value);
}
map.TryGetValue("JAVA_VERSION", out var version);
if (string.IsNullOrWhiteSpace(version) && map.TryGetValue("JAVA_RUNTIME_VERSION", out var runtimeVersion))
{
version = runtimeVersion;
}
map.TryGetValue("IMPLEMENTOR", out var vendor);
if (string.IsNullOrWhiteSpace(vendor) && map.TryGetValue("IMPLEMENTOR_VERSION", out var implementorVersion))
{
vendor = implementorVersion;
}
return new JavaReleaseMetadata(
version?.Trim() ?? string.Empty,
vendor?.Trim() ?? string.Empty);
}
private static string TrimQuotes(string value)
{
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
{
return value[1..^1];
}
return value;
}
public sealed record JavaReleaseMetadata(string Version, string Vendor);
}
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaReleaseFileParser
{
public static JavaReleaseMetadata Parse(string filePath)
{
ArgumentException.ThrowIfNullOrEmpty(filePath);
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? line;
while ((line = reader.ReadLine()) is not null)
{
line = line.Trim();
if (line.Length == 0 || line.StartsWith('#'))
{
continue;
}
var separatorIndex = line.IndexOf('=');
if (separatorIndex <= 0)
{
continue;
}
var key = line[..separatorIndex].Trim();
if (key.Length == 0)
{
continue;
}
var value = line[(separatorIndex + 1)..].Trim();
map[key] = TrimQuotes(value);
}
map.TryGetValue("JAVA_VERSION", out var version);
if (string.IsNullOrWhiteSpace(version) && map.TryGetValue("JAVA_RUNTIME_VERSION", out var runtimeVersion))
{
version = runtimeVersion;
}
map.TryGetValue("IMPLEMENTOR", out var vendor);
if (string.IsNullOrWhiteSpace(vendor) && map.TryGetValue("IMPLEMENTOR_VERSION", out var implementorVersion))
{
vendor = implementorVersion;
}
return new JavaReleaseMetadata(
version?.Trim() ?? string.Empty,
vendor?.Trim() ?? string.Empty);
}
private static string TrimQuotes(string value)
{
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
{
return value[1..^1];
}
return value;
}
public sealed record JavaReleaseMetadata(string Version, string Vendor);
}

View File

@@ -1,7 +1,7 @@
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed record JavaRuntimeImage(
string AbsolutePath,
string RelativePath,
string JavaVersion,
string Vendor);
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed record JavaRuntimeImage(
string AbsolutePath,
string RelativePath,
string JavaVersion,
string Vendor);

View File

@@ -1,28 +1,28 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed class JavaWorkspace
{
public JavaWorkspace(
IEnumerable<JavaArchive> archives,
IEnumerable<JavaRuntimeImage> runtimeImages)
{
ArgumentNullException.ThrowIfNull(archives);
ArgumentNullException.ThrowIfNull(runtimeImages);
Archives = archives
.Where(static archive => archive is not null)
.OrderBy(static archive => archive.RelativePath, StringComparer.Ordinal)
.ToImmutableArray();
RuntimeImages = runtimeImages
.Where(static image => image is not null)
.OrderBy(static image => image.RelativePath, StringComparer.Ordinal)
.ToImmutableArray();
}
public ImmutableArray<JavaArchive> Archives { get; }
public ImmutableArray<JavaRuntimeImage> RuntimeImages { get; }
}
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal sealed class JavaWorkspace
{
public JavaWorkspace(
IEnumerable<JavaArchive> archives,
IEnumerable<JavaRuntimeImage> runtimeImages)
{
ArgumentNullException.ThrowIfNull(archives);
ArgumentNullException.ThrowIfNull(runtimeImages);
Archives = archives
.Where(static archive => archive is not null)
.OrderBy(static archive => archive.RelativePath, StringComparer.Ordinal)
.ToImmutableArray();
RuntimeImages = runtimeImages
.Where(static image => image is not null)
.OrderBy(static image => image.RelativePath, StringComparer.Ordinal)
.ToImmutableArray();
}
public ImmutableArray<JavaArchive> Archives { get; }
public ImmutableArray<JavaRuntimeImage> RuntimeImages { get; }
}

View File

@@ -1,101 +1,101 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaWorkspaceNormalizer
{
private static readonly HashSet<string> SupportedExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".jar",
".war",
".ear",
".jmod",
".jimage",
};
private static readonly EnumerationOptions EnumerationOptions = new()
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
ReturnSpecialDirectories = false,
};
public static JavaWorkspace Normalize(LanguageAnalyzerContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var archives = new List<JavaArchive>();
var runtimeImages = new List<JavaRuntimeImage>();
foreach (var filePath in Directory.EnumerateFiles(context.RootPath, "*", EnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
if (!SupportedExtensions.Contains(Path.GetExtension(filePath)))
{
continue;
}
try
{
var relative = context.GetRelativePath(filePath);
var archive = JavaArchive.Load(filePath, relative);
archives.Add(archive);
}
catch (IOException)
{
// Corrupt archives should not abort the scan.
}
catch (InvalidDataException)
{
// Skip non-zip payloads despite the extension.
}
}
foreach (var directory in Directory.EnumerateDirectories(context.RootPath, "*", EnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
if (!LooksLikeRuntimeImage(directory))
{
continue;
}
var releasePath = Path.Combine(directory, "release");
if (!File.Exists(releasePath))
{
continue;
}
var metadata = JavaReleaseFileParser.Parse(releasePath);
runtimeImages.Add(new JavaRuntimeImage(
AbsolutePath: directory,
RelativePath: context.GetRelativePath(directory),
JavaVersion: metadata.Version,
Vendor: metadata.Vendor));
}
catch (IOException)
{
// Skip directories we cannot access.
}
}
return new JavaWorkspace(archives, runtimeImages);
}
private static bool LooksLikeRuntimeImage(string directory)
{
if (!Directory.Exists(directory))
{
return false;
}
var libModules = Path.Combine(directory, "lib", "modules");
var binJava = Path.Combine(directory, "bin", OperatingSystem.IsWindows() ? "java.exe" : "java");
return File.Exists(libModules) || File.Exists(binJava);
}
}
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaWorkspaceNormalizer
{
private static readonly HashSet<string> SupportedExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".jar",
".war",
".ear",
".jmod",
".jimage",
};
private static readonly EnumerationOptions EnumerationOptions = new()
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
ReturnSpecialDirectories = false,
};
public static JavaWorkspace Normalize(LanguageAnalyzerContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var archives = new List<JavaArchive>();
var runtimeImages = new List<JavaRuntimeImage>();
foreach (var filePath in Directory.EnumerateFiles(context.RootPath, "*", EnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
if (!SupportedExtensions.Contains(Path.GetExtension(filePath)))
{
continue;
}
try
{
var relative = context.GetRelativePath(filePath);
var archive = JavaArchive.Load(filePath, relative);
archives.Add(archive);
}
catch (IOException)
{
// Corrupt archives should not abort the scan.
}
catch (InvalidDataException)
{
// Skip non-zip payloads despite the extension.
}
}
foreach (var directory in Directory.EnumerateDirectories(context.RootPath, "*", EnumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
if (!LooksLikeRuntimeImage(directory))
{
continue;
}
var releasePath = Path.Combine(directory, "release");
if (!File.Exists(releasePath))
{
continue;
}
var metadata = JavaReleaseFileParser.Parse(releasePath);
runtimeImages.Add(new JavaRuntimeImage(
AbsolutePath: directory,
RelativePath: context.GetRelativePath(directory),
JavaVersion: metadata.Version,
Vendor: metadata.Vendor));
}
catch (IOException)
{
// Skip directories we cannot access.
}
}
return new JavaWorkspace(archives, runtimeImages);
}
private static bool LooksLikeRuntimeImage(string directory)
{
if (!Directory.Exists(directory))
{
return false;
}
var libModules = Path.Combine(directory, "lib", "modules");
var binJava = Path.Combine(directory, "bin", OperatingSystem.IsWindows() ? "java.exe" : "java");
return File.Exists(libModules) || File.Exists(binJava);
}
}

View File

@@ -1,52 +1,52 @@
using System.Globalization;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaZipEntryUtilities
{
public static string NormalizeEntryName(string entryName)
{
var normalized = entryName.Replace('\\', '/');
return normalized.TrimStart('/');
}
public static bool TryParseMultiReleasePath(string normalizedPath, out string effectivePath, out int version)
{
const string Prefix = "META-INF/versions/";
if (!normalizedPath.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var remainder = normalizedPath.AsSpan(Prefix.Length);
var separatorIndex = remainder.IndexOf('/');
if (separatorIndex <= 0)
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var versionSpan = remainder[..separatorIndex];
if (!int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedVersion))
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var relativeSpan = remainder[(separatorIndex + 1)..];
if (relativeSpan.IsEmpty)
{
effectivePath = normalizedPath;
version = 0;
return false;
}
effectivePath = relativeSpan.ToString();
version = parsedVersion;
return true;
}
}
using System.Globalization;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaZipEntryUtilities
{
public static string NormalizeEntryName(string entryName)
{
var normalized = entryName.Replace('\\', '/');
return normalized.TrimStart('/');
}
public static bool TryParseMultiReleasePath(string normalizedPath, out string effectivePath, out int version)
{
const string Prefix = "META-INF/versions/";
if (!normalizedPath.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var remainder = normalizedPath.AsSpan(Prefix.Length);
var separatorIndex = remainder.IndexOf('/');
if (separatorIndex <= 0)
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var versionSpan = remainder[..separatorIndex];
if (!int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedVersion))
{
effectivePath = normalizedPath;
version = 0;
return false;
}
var relativeSpan = remainder[(separatorIndex + 1)..];
if (relativeSpan.IsEmpty)
{
effectivePath = normalizedPath;
version = 0;
return false;
}
effectivePath = relativeSpan.ToString();
version = parsedVersion;
return true;
}
}

View File

@@ -1,44 +1,44 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
internal sealed record JavaReflectionAnalysis(
ImmutableArray<JavaReflectionEdge> Edges,
ImmutableArray<JavaReflectionWarning> Warnings)
{
public static readonly JavaReflectionAnalysis Empty = new(ImmutableArray<JavaReflectionEdge>.Empty, ImmutableArray<JavaReflectionWarning>.Empty);
}
internal sealed record JavaReflectionEdge(
string SourceClass,
string SegmentIdentifier,
string? TargetType,
JavaReflectionReason Reason,
JavaReflectionConfidence Confidence,
string MethodName,
string MethodDescriptor,
int InstructionOffset,
string? Details);
internal sealed record JavaReflectionWarning(
string SourceClass,
string SegmentIdentifier,
string WarningCode,
string Message,
string MethodName,
string MethodDescriptor);
internal enum JavaReflectionReason
{
ClassForName,
ClassLoaderLoadClass,
ServiceLoaderLoad,
ResourceLookup,
}
internal enum JavaReflectionConfidence
{
Low = 1,
Medium = 2,
High = 3,
}
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
internal sealed record JavaReflectionAnalysis(
ImmutableArray<JavaReflectionEdge> Edges,
ImmutableArray<JavaReflectionWarning> Warnings)
{
public static readonly JavaReflectionAnalysis Empty = new(ImmutableArray<JavaReflectionEdge>.Empty, ImmutableArray<JavaReflectionWarning>.Empty);
}
internal sealed record JavaReflectionEdge(
string SourceClass,
string SegmentIdentifier,
string? TargetType,
JavaReflectionReason Reason,
JavaReflectionConfidence Confidence,
string MethodName,
string MethodDescriptor,
int InstructionOffset,
string? Details);
internal sealed record JavaReflectionWarning(
string SourceClass,
string SegmentIdentifier,
string WarningCode,
string Message,
string MethodName,
string MethodDescriptor);
internal enum JavaReflectionReason
{
ClassForName,
ClassLoaderLoadClass,
ServiceLoaderLoad,
ResourceLookup,
}
internal enum JavaReflectionConfidence
{
Low = 1,
Medium = 2,
High = 3,
}

View File

@@ -1,160 +1,160 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
internal static class JavaServiceProviderScanner
{
public static JavaServiceProviderAnalysis Scan(JavaClassPathAnalysis classPath, JavaSpiCatalog catalog, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(classPath);
ArgumentNullException.ThrowIfNull(catalog);
var services = new Dictionary<string, ServiceAccumulator>(StringComparer.Ordinal);
foreach (var segment in classPath.Segments.OrderBy(static s => s.Order))
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var kvp in segment.ServiceDefinitions)
{
cancellationToken.ThrowIfCancellationRequested();
if (kvp.Value.IsDefaultOrEmpty)
{
continue;
}
if (!services.TryGetValue(kvp.Key, out var accumulator))
{
accumulator = new ServiceAccumulator();
services[kvp.Key] = accumulator;
}
var providerIndex = 0;
foreach (var provider in kvp.Value)
{
var normalizedProvider = provider?.Trim();
if (string.IsNullOrEmpty(normalizedProvider))
{
providerIndex++;
continue;
}
accumulator.AddCandidate(new JavaServiceProviderCandidateRecord(
ProviderClass: normalizedProvider,
SegmentIdentifier: segment.Identifier,
SegmentOrder: segment.Order,
ProviderIndex: providerIndex++,
IsSelected: false));
}
}
}
var records = new List<JavaServiceProviderRecord>(services.Count);
foreach (var pair in services.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
{
var descriptor = catalog.Resolve(pair.Key);
var accumulator = pair.Value;
var orderedCandidates = accumulator.GetOrderedCandidates();
if (orderedCandidates.Count == 0)
{
continue;
}
var selectedIndex = accumulator.DetermineSelection(orderedCandidates);
var warnings = accumulator.BuildWarnings();
var candidateArray = ImmutableArray.CreateRange(orderedCandidates.Select((candidate, index) =>
candidate with { IsSelected = index == selectedIndex }));
var warningsArray = warnings.Count == 0
? ImmutableArray<string>.Empty
: ImmutableArray.CreateRange(warnings);
records.Add(new JavaServiceProviderRecord(
ServiceId: pair.Key,
DisplayName: descriptor.DisplayName,
Category: descriptor.Category,
Candidates: candidateArray,
SelectedIndex: selectedIndex,
Warnings: warningsArray));
}
return new JavaServiceProviderAnalysis(records.ToImmutableArray());
}
private sealed class ServiceAccumulator
{
private readonly List<JavaServiceProviderCandidateRecord> _candidates = new();
private readonly Dictionary<string, HashSet<string>> _providerSources = new(StringComparer.Ordinal);
public void AddCandidate(JavaServiceProviderCandidateRecord candidate)
{
_candidates.Add(candidate);
if (!_providerSources.TryGetValue(candidate.ProviderClass, out var sources))
{
sources = new HashSet<string>(StringComparer.Ordinal);
_providerSources[candidate.ProviderClass] = sources;
}
sources.Add(candidate.SegmentIdentifier);
}
public IReadOnlyList<JavaServiceProviderCandidateRecord> GetOrderedCandidates()
=> _candidates
.OrderBy(static c => c.SegmentOrder)
.ThenBy(static c => c.ProviderIndex)
.ThenBy(static c => c.ProviderClass, StringComparer.Ordinal)
.ToList();
public int DetermineSelection(IReadOnlyList<JavaServiceProviderCandidateRecord> orderedCandidates)
=> orderedCandidates.Count == 0 ? -1 : 0;
public List<string> BuildWarnings()
{
var warnings = new List<string>();
foreach (var pair in _providerSources.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
{
if (pair.Value.Count <= 1)
{
continue;
}
var locations = pair.Value
.OrderBy(static value => value, StringComparer.Ordinal)
.ToArray();
warnings.Add($"duplicate-provider: {pair.Key} ({string.Join(", ", locations)})");
}
return warnings;
}
}
}
internal sealed record JavaServiceProviderAnalysis(ImmutableArray<JavaServiceProviderRecord> Services)
{
public static readonly JavaServiceProviderAnalysis Empty = new(ImmutableArray<JavaServiceProviderRecord>.Empty);
}
internal sealed record JavaServiceProviderRecord(
string ServiceId,
string DisplayName,
string Category,
ImmutableArray<JavaServiceProviderCandidateRecord> Candidates,
int SelectedIndex,
ImmutableArray<string> Warnings);
internal sealed record JavaServiceProviderCandidateRecord(
string ProviderClass,
string SegmentIdentifier,
int SegmentOrder,
int ProviderIndex,
bool IsSelected);
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
internal static class JavaServiceProviderScanner
{
public static JavaServiceProviderAnalysis Scan(JavaClassPathAnalysis classPath, JavaSpiCatalog catalog, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(classPath);
ArgumentNullException.ThrowIfNull(catalog);
var services = new Dictionary<string, ServiceAccumulator>(StringComparer.Ordinal);
foreach (var segment in classPath.Segments.OrderBy(static s => s.Order))
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var kvp in segment.ServiceDefinitions)
{
cancellationToken.ThrowIfCancellationRequested();
if (kvp.Value.IsDefaultOrEmpty)
{
continue;
}
if (!services.TryGetValue(kvp.Key, out var accumulator))
{
accumulator = new ServiceAccumulator();
services[kvp.Key] = accumulator;
}
var providerIndex = 0;
foreach (var provider in kvp.Value)
{
var normalizedProvider = provider?.Trim();
if (string.IsNullOrEmpty(normalizedProvider))
{
providerIndex++;
continue;
}
accumulator.AddCandidate(new JavaServiceProviderCandidateRecord(
ProviderClass: normalizedProvider,
SegmentIdentifier: segment.Identifier,
SegmentOrder: segment.Order,
ProviderIndex: providerIndex++,
IsSelected: false));
}
}
}
var records = new List<JavaServiceProviderRecord>(services.Count);
foreach (var pair in services.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
{
var descriptor = catalog.Resolve(pair.Key);
var accumulator = pair.Value;
var orderedCandidates = accumulator.GetOrderedCandidates();
if (orderedCandidates.Count == 0)
{
continue;
}
var selectedIndex = accumulator.DetermineSelection(orderedCandidates);
var warnings = accumulator.BuildWarnings();
var candidateArray = ImmutableArray.CreateRange(orderedCandidates.Select((candidate, index) =>
candidate with { IsSelected = index == selectedIndex }));
var warningsArray = warnings.Count == 0
? ImmutableArray<string>.Empty
: ImmutableArray.CreateRange(warnings);
records.Add(new JavaServiceProviderRecord(
ServiceId: pair.Key,
DisplayName: descriptor.DisplayName,
Category: descriptor.Category,
Candidates: candidateArray,
SelectedIndex: selectedIndex,
Warnings: warningsArray));
}
return new JavaServiceProviderAnalysis(records.ToImmutableArray());
}
private sealed class ServiceAccumulator
{
private readonly List<JavaServiceProviderCandidateRecord> _candidates = new();
private readonly Dictionary<string, HashSet<string>> _providerSources = new(StringComparer.Ordinal);
public void AddCandidate(JavaServiceProviderCandidateRecord candidate)
{
_candidates.Add(candidate);
if (!_providerSources.TryGetValue(candidate.ProviderClass, out var sources))
{
sources = new HashSet<string>(StringComparer.Ordinal);
_providerSources[candidate.ProviderClass] = sources;
}
sources.Add(candidate.SegmentIdentifier);
}
public IReadOnlyList<JavaServiceProviderCandidateRecord> GetOrderedCandidates()
=> _candidates
.OrderBy(static c => c.SegmentOrder)
.ThenBy(static c => c.ProviderIndex)
.ThenBy(static c => c.ProviderClass, StringComparer.Ordinal)
.ToList();
public int DetermineSelection(IReadOnlyList<JavaServiceProviderCandidateRecord> orderedCandidates)
=> orderedCandidates.Count == 0 ? -1 : 0;
public List<string> BuildWarnings()
{
var warnings = new List<string>();
foreach (var pair in _providerSources.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
{
if (pair.Value.Count <= 1)
{
continue;
}
var locations = pair.Value
.OrderBy(static value => value, StringComparer.Ordinal)
.ToArray();
warnings.Add($"duplicate-provider: {pair.Key} ({string.Join(", ", locations)})");
}
return warnings;
}
}
}
internal sealed record JavaServiceProviderAnalysis(ImmutableArray<JavaServiceProviderRecord> Services)
{
public static readonly JavaServiceProviderAnalysis Empty = new(ImmutableArray<JavaServiceProviderRecord>.Empty);
}
internal sealed record JavaServiceProviderRecord(
string ServiceId,
string DisplayName,
string Category,
ImmutableArray<JavaServiceProviderCandidateRecord> Candidates,
int SelectedIndex,
ImmutableArray<string> Warnings);
internal sealed record JavaServiceProviderCandidateRecord(
string ProviderClass,
string SegmentIdentifier,
int SegmentOrder,
int ProviderIndex,
bool IsSelected);

View File

@@ -1,103 +1,103 @@
using System.Collections.Immutable;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
internal sealed class JavaSpiCatalog
{
private static readonly Lazy<JavaSpiCatalog> LazyDefault = new(LoadDefaultCore);
private readonly ImmutableDictionary<string, JavaSpiDescriptor> _descriptors;
private JavaSpiCatalog(ImmutableDictionary<string, JavaSpiDescriptor> descriptors)
{
_descriptors = descriptors;
}
public static JavaSpiCatalog Default => LazyDefault.Value;
public JavaSpiDescriptor Resolve(string serviceId)
{
if (string.IsNullOrWhiteSpace(serviceId))
{
return JavaSpiDescriptor.CreateUnknown(string.Empty);
}
var key = serviceId.Trim();
if (_descriptors.TryGetValue(key, out var descriptor))
{
return descriptor;
}
return JavaSpiDescriptor.CreateUnknown(key);
}
private static JavaSpiCatalog LoadDefaultCore()
{
var assembly = typeof(JavaSpiCatalog).GetTypeInfo().Assembly;
var resourceName = "StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders.java-spi-catalog.json";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded SPI catalog '{resourceName}' not found.");
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: false);
var json = reader.ReadToEnd();
var items = JsonSerializer.Deserialize<List<JavaSpiDescriptor>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
}) ?? new List<JavaSpiDescriptor>();
var descriptors = items
.Select(Normalize)
.Where(static item => !string.IsNullOrWhiteSpace(item.ServiceId))
.ToImmutableDictionary(
static item => item.ServiceId,
static item => item,
StringComparer.Ordinal);
return new JavaSpiCatalog(descriptors);
}
private static JavaSpiDescriptor Normalize(JavaSpiDescriptor descriptor)
{
var serviceId = descriptor.ServiceId?.Trim() ?? string.Empty;
var category = string.IsNullOrWhiteSpace(descriptor.Category)
? "unknown"
: descriptor.Category.Trim().ToLowerInvariant();
var displayName = string.IsNullOrWhiteSpace(descriptor.DisplayName)
? serviceId
: descriptor.DisplayName.Trim();
return descriptor with
{
ServiceId = serviceId,
Category = category,
DisplayName = displayName,
};
}
}
internal sealed record class JavaSpiDescriptor
{
[JsonPropertyName("serviceId")]
public string ServiceId { get; init; } = string.Empty;
[JsonPropertyName("category")]
public string Category { get; init; } = "unknown";
[JsonPropertyName("displayName")]
public string DisplayName { get; init; } = string.Empty;
[JsonPropertyName("notes")]
public string? Notes { get; init; }
public static JavaSpiDescriptor CreateUnknown(string serviceId)
=> new()
{
ServiceId = serviceId,
Category = "unknown",
DisplayName = serviceId,
};
}
using System.Collections.Immutable;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
internal sealed class JavaSpiCatalog
{
private static readonly Lazy<JavaSpiCatalog> LazyDefault = new(LoadDefaultCore);
private readonly ImmutableDictionary<string, JavaSpiDescriptor> _descriptors;
private JavaSpiCatalog(ImmutableDictionary<string, JavaSpiDescriptor> descriptors)
{
_descriptors = descriptors;
}
public static JavaSpiCatalog Default => LazyDefault.Value;
public JavaSpiDescriptor Resolve(string serviceId)
{
if (string.IsNullOrWhiteSpace(serviceId))
{
return JavaSpiDescriptor.CreateUnknown(string.Empty);
}
var key = serviceId.Trim();
if (_descriptors.TryGetValue(key, out var descriptor))
{
return descriptor;
}
return JavaSpiDescriptor.CreateUnknown(key);
}
private static JavaSpiCatalog LoadDefaultCore()
{
var assembly = typeof(JavaSpiCatalog).GetTypeInfo().Assembly;
var resourceName = "StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders.java-spi-catalog.json";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded SPI catalog '{resourceName}' not found.");
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: false);
var json = reader.ReadToEnd();
var items = JsonSerializer.Deserialize<List<JavaSpiDescriptor>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
}) ?? new List<JavaSpiDescriptor>();
var descriptors = items
.Select(Normalize)
.Where(static item => !string.IsNullOrWhiteSpace(item.ServiceId))
.ToImmutableDictionary(
static item => item.ServiceId,
static item => item,
StringComparer.Ordinal);
return new JavaSpiCatalog(descriptors);
}
private static JavaSpiDescriptor Normalize(JavaSpiDescriptor descriptor)
{
var serviceId = descriptor.ServiceId?.Trim() ?? string.Empty;
var category = string.IsNullOrWhiteSpace(descriptor.Category)
? "unknown"
: descriptor.Category.Trim().ToLowerInvariant();
var displayName = string.IsNullOrWhiteSpace(descriptor.DisplayName)
? serviceId
: descriptor.DisplayName.Trim();
return descriptor with
{
ServiceId = serviceId,
Category = category,
DisplayName = displayName,
};
}
}
internal sealed record class JavaSpiDescriptor
{
[JsonPropertyName("serviceId")]
public string ServiceId { get; init; } = string.Empty;
[JsonPropertyName("category")]
public string Category { get; init; } = "unknown";
[JsonPropertyName("displayName")]
public string DisplayName { get; init; } = string.Empty;
[JsonPropertyName("notes")]
public string? Notes { get; init; }
public static JavaSpiDescriptor CreateUnknown(string serviceId)
=> new()
{
ServiceId = serviceId,
Category = "unknown",
DisplayName = serviceId,
};
}

View File

@@ -1,3 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Scanner.Analyzers.Lang.Java.Tests")]
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Scanner.Analyzers.Lang.Java.Tests")]