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
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user