Align AOC tasks for Excititor and Concelier
This commit is contained in:
@@ -1,298 +1,298 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal;
|
||||
|
||||
internal static class RustLicenseScanner
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, RustLicenseIndex> IndexCache = new(StringComparer.Ordinal);
|
||||
|
||||
public static RustLicenseIndex GetOrCreate(string rootPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath) || !Directory.Exists(rootPath))
|
||||
{
|
||||
return RustLicenseIndex.Empty;
|
||||
}
|
||||
|
||||
var normalizedRoot = NormalizeRoot(rootPath);
|
||||
return IndexCache.GetOrAdd(
|
||||
normalizedRoot,
|
||||
static (_, state) => BuildIndex(state.RootPath, state.CancellationToken),
|
||||
(RootPath: rootPath, CancellationToken: cancellationToken));
|
||||
}
|
||||
|
||||
private static RustLicenseIndex BuildIndex(string rootPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var byName = new Dictionary<string, List<RustLicenseInfo>>(StringComparer.Ordinal);
|
||||
var enumeration = new EnumerationOptions
|
||||
{
|
||||
MatchCasing = MatchCasing.CaseSensitive,
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = true,
|
||||
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
|
||||
};
|
||||
|
||||
foreach (var cargoTomlPath in Directory.EnumerateFiles(rootPath, "Cargo.toml", enumeration))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsUnderTargetDirectory(cargoTomlPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseCargoToml(rootPath, cargoTomlPath, out var info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedName = RustCrateBuilder.NormalizeName(info.Name);
|
||||
if (!byName.TryGetValue(normalizedName, out var entries))
|
||||
{
|
||||
entries = new List<RustLicenseInfo>();
|
||||
byName[normalizedName] = entries;
|
||||
}
|
||||
|
||||
entries.Add(info);
|
||||
}
|
||||
|
||||
foreach (var entry in byName.Values)
|
||||
{
|
||||
entry.Sort(static (left, right) =>
|
||||
{
|
||||
var versionCompare = string.Compare(left.Version, right.Version, StringComparison.OrdinalIgnoreCase);
|
||||
if (versionCompare != 0)
|
||||
{
|
||||
return versionCompare;
|
||||
}
|
||||
|
||||
return string.Compare(left.CargoTomlRelativePath, right.CargoTomlRelativePath, StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
return new RustLicenseIndex(byName);
|
||||
}
|
||||
|
||||
private static bool TryParseCargoToml(string rootPath, string cargoTomlPath, out RustLicenseInfo info)
|
||||
{
|
||||
info = default!;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(cargoTomlPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new StreamReader(stream, leaveOpen: false);
|
||||
|
||||
string? name = null;
|
||||
string? version = null;
|
||||
string? licenseExpression = null;
|
||||
string? licenseFile = null;
|
||||
var inPackageSection = false;
|
||||
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
line = StripComment(line).Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
inPackageSection = string.Equals(line, "[package]", StringComparison.OrdinalIgnoreCase);
|
||||
if (!inPackageSection && line.StartsWith("[dependency", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Exiting package section.
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inPackageSection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "name", out var parsedName))
|
||||
{
|
||||
name ??= parsedName;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "version", out var parsedVersion))
|
||||
{
|
||||
version ??= parsedVersion;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "license", out var parsedLicense))
|
||||
{
|
||||
licenseExpression ??= parsedLicense;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "license-file", out var parsedLicenseFile))
|
||||
{
|
||||
licenseFile ??= parsedLicenseFile;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expressions = ImmutableArray<string>.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(licenseExpression))
|
||||
{
|
||||
expressions = ImmutableArray.Create(licenseExpression!);
|
||||
}
|
||||
|
||||
var files = ImmutableArray<RustLicenseFileReference>.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(licenseFile))
|
||||
{
|
||||
var directory = Path.GetDirectoryName(cargoTomlPath) ?? string.Empty;
|
||||
var absolute = Path.GetFullPath(Path.Combine(directory, licenseFile!));
|
||||
if (File.Exists(absolute))
|
||||
{
|
||||
var relative = NormalizeRelativePath(rootPath, absolute);
|
||||
if (RustFileHashCache.TryGetSha256(absolute, out var sha256))
|
||||
{
|
||||
files = ImmutableArray.Create(new RustLicenseFileReference(relative, sha256));
|
||||
}
|
||||
else
|
||||
{
|
||||
files = ImmutableArray.Create(new RustLicenseFileReference(relative, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cargoRelative = NormalizeRelativePath(rootPath, cargoTomlPath);
|
||||
|
||||
info = new RustLicenseInfo(
|
||||
name!.Trim(),
|
||||
string.IsNullOrWhiteSpace(version) ? null : version!.Trim(),
|
||||
expressions,
|
||||
files,
|
||||
cargoRelative);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeRoot(string rootPath)
|
||||
{
|
||||
var full = Path.GetFullPath(rootPath);
|
||||
return OperatingSystem.IsWindows()
|
||||
? full.ToLowerInvariant()
|
||||
: full;
|
||||
}
|
||||
|
||||
private static bool TryParseStringAssignment(string line, string key, out string? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (!line.StartsWith(key, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var remaining = line[key.Length..].TrimStart();
|
||||
if (remaining.Length == 0 || remaining[0] != '=')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining = remaining[1..].TrimStart();
|
||||
if (remaining.Length < 2 || remaining[0] != '"' || remaining[^1] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = remaining[1..^1];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string StripComment(string line)
|
||||
{
|
||||
var index = line.IndexOf('#');
|
||||
return index < 0 ? line : line[..index];
|
||||
}
|
||||
|
||||
private static bool IsUnderTargetDirectory(string path)
|
||||
{
|
||||
var segment = $"{Path.DirectorySeparatorChar}target{Path.DirectorySeparatorChar}";
|
||||
return path.Contains(segment, OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string rootPath, string absolutePath)
|
||||
{
|
||||
var relative = Path.GetRelativePath(rootPath, absolutePath);
|
||||
if (string.IsNullOrWhiteSpace(relative) || relative == ".")
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
return relative.Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RustLicenseIndex
|
||||
{
|
||||
private readonly Dictionary<string, List<RustLicenseInfo>> _byName;
|
||||
|
||||
public static readonly RustLicenseIndex Empty = new(new Dictionary<string, List<RustLicenseInfo>>(StringComparer.Ordinal));
|
||||
|
||||
public RustLicenseIndex(Dictionary<string, List<RustLicenseInfo>> byName)
|
||||
{
|
||||
_byName = byName ?? throw new ArgumentNullException(nameof(byName));
|
||||
}
|
||||
|
||||
public RustLicenseInfo? Find(string crateName, string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(crateName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = RustCrateBuilder.NormalizeName(crateName);
|
||||
if (!_byName.TryGetValue(normalized, out var list) || list.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
var match = list.FirstOrDefault(entry => string.Equals(entry.Version, version, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is not null)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record RustLicenseInfo(
|
||||
string Name,
|
||||
string? Version,
|
||||
ImmutableArray<string> Expressions,
|
||||
ImmutableArray<RustLicenseFileReference> Files,
|
||||
string CargoTomlRelativePath);
|
||||
|
||||
internal sealed record RustLicenseFileReference(string RelativePath, string? Sha256);
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal;
|
||||
|
||||
internal static class RustLicenseScanner
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, RustLicenseIndex> IndexCache = new(StringComparer.Ordinal);
|
||||
|
||||
public static RustLicenseIndex GetOrCreate(string rootPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath) || !Directory.Exists(rootPath))
|
||||
{
|
||||
return RustLicenseIndex.Empty;
|
||||
}
|
||||
|
||||
var normalizedRoot = NormalizeRoot(rootPath);
|
||||
return IndexCache.GetOrAdd(
|
||||
normalizedRoot,
|
||||
static (_, state) => BuildIndex(state.RootPath, state.CancellationToken),
|
||||
(RootPath: rootPath, CancellationToken: cancellationToken));
|
||||
}
|
||||
|
||||
private static RustLicenseIndex BuildIndex(string rootPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var byName = new Dictionary<string, List<RustLicenseInfo>>(StringComparer.Ordinal);
|
||||
var enumeration = new EnumerationOptions
|
||||
{
|
||||
MatchCasing = MatchCasing.CaseSensitive,
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = true,
|
||||
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
|
||||
};
|
||||
|
||||
foreach (var cargoTomlPath in Directory.EnumerateFiles(rootPath, "Cargo.toml", enumeration))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsUnderTargetDirectory(cargoTomlPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseCargoToml(rootPath, cargoTomlPath, out var info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedName = RustCrateBuilder.NormalizeName(info.Name);
|
||||
if (!byName.TryGetValue(normalizedName, out var entries))
|
||||
{
|
||||
entries = new List<RustLicenseInfo>();
|
||||
byName[normalizedName] = entries;
|
||||
}
|
||||
|
||||
entries.Add(info);
|
||||
}
|
||||
|
||||
foreach (var entry in byName.Values)
|
||||
{
|
||||
entry.Sort(static (left, right) =>
|
||||
{
|
||||
var versionCompare = string.Compare(left.Version, right.Version, StringComparison.OrdinalIgnoreCase);
|
||||
if (versionCompare != 0)
|
||||
{
|
||||
return versionCompare;
|
||||
}
|
||||
|
||||
return string.Compare(left.CargoTomlRelativePath, right.CargoTomlRelativePath, StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
return new RustLicenseIndex(byName);
|
||||
}
|
||||
|
||||
private static bool TryParseCargoToml(string rootPath, string cargoTomlPath, out RustLicenseInfo info)
|
||||
{
|
||||
info = default!;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(cargoTomlPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new StreamReader(stream, leaveOpen: false);
|
||||
|
||||
string? name = null;
|
||||
string? version = null;
|
||||
string? licenseExpression = null;
|
||||
string? licenseFile = null;
|
||||
var inPackageSection = false;
|
||||
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
line = StripComment(line).Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
inPackageSection = string.Equals(line, "[package]", StringComparison.OrdinalIgnoreCase);
|
||||
if (!inPackageSection && line.StartsWith("[dependency", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Exiting package section.
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inPackageSection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "name", out var parsedName))
|
||||
{
|
||||
name ??= parsedName;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "version", out var parsedVersion))
|
||||
{
|
||||
version ??= parsedVersion;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "license", out var parsedLicense))
|
||||
{
|
||||
licenseExpression ??= parsedLicense;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseStringAssignment(line, "license-file", out var parsedLicenseFile))
|
||||
{
|
||||
licenseFile ??= parsedLicenseFile;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expressions = ImmutableArray<string>.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(licenseExpression))
|
||||
{
|
||||
expressions = ImmutableArray.Create(licenseExpression!);
|
||||
}
|
||||
|
||||
var files = ImmutableArray<RustLicenseFileReference>.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(licenseFile))
|
||||
{
|
||||
var directory = Path.GetDirectoryName(cargoTomlPath) ?? string.Empty;
|
||||
var absolute = Path.GetFullPath(Path.Combine(directory, licenseFile!));
|
||||
if (File.Exists(absolute))
|
||||
{
|
||||
var relative = NormalizeRelativePath(rootPath, absolute);
|
||||
if (RustFileHashCache.TryGetSha256(absolute, out var sha256))
|
||||
{
|
||||
files = ImmutableArray.Create(new RustLicenseFileReference(relative, sha256));
|
||||
}
|
||||
else
|
||||
{
|
||||
files = ImmutableArray.Create(new RustLicenseFileReference(relative, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cargoRelative = NormalizeRelativePath(rootPath, cargoTomlPath);
|
||||
|
||||
info = new RustLicenseInfo(
|
||||
name!.Trim(),
|
||||
string.IsNullOrWhiteSpace(version) ? null : version!.Trim(),
|
||||
expressions,
|
||||
files,
|
||||
cargoRelative);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeRoot(string rootPath)
|
||||
{
|
||||
var full = Path.GetFullPath(rootPath);
|
||||
return OperatingSystem.IsWindows()
|
||||
? full.ToLowerInvariant()
|
||||
: full;
|
||||
}
|
||||
|
||||
private static bool TryParseStringAssignment(string line, string key, out string? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (!line.StartsWith(key, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var remaining = line[key.Length..].TrimStart();
|
||||
if (remaining.Length == 0 || remaining[0] != '=')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining = remaining[1..].TrimStart();
|
||||
if (remaining.Length < 2 || remaining[0] != '"' || remaining[^1] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = remaining[1..^1];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string StripComment(string line)
|
||||
{
|
||||
var index = line.IndexOf('#');
|
||||
return index < 0 ? line : line[..index];
|
||||
}
|
||||
|
||||
private static bool IsUnderTargetDirectory(string path)
|
||||
{
|
||||
var segment = $"{Path.DirectorySeparatorChar}target{Path.DirectorySeparatorChar}";
|
||||
return path.Contains(segment, OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string rootPath, string absolutePath)
|
||||
{
|
||||
var relative = Path.GetRelativePath(rootPath, absolutePath);
|
||||
if (string.IsNullOrWhiteSpace(relative) || relative == ".")
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
return relative.Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RustLicenseIndex
|
||||
{
|
||||
private readonly Dictionary<string, List<RustLicenseInfo>> _byName;
|
||||
|
||||
public static readonly RustLicenseIndex Empty = new(new Dictionary<string, List<RustLicenseInfo>>(StringComparer.Ordinal));
|
||||
|
||||
public RustLicenseIndex(Dictionary<string, List<RustLicenseInfo>> byName)
|
||||
{
|
||||
_byName = byName ?? throw new ArgumentNullException(nameof(byName));
|
||||
}
|
||||
|
||||
public RustLicenseInfo? Find(string crateName, string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(crateName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = RustCrateBuilder.NormalizeName(crateName);
|
||||
if (!_byName.TryGetValue(normalized, out var list) || list.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
var match = list.FirstOrDefault(entry => string.Equals(entry.Version, version, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is not null)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record RustLicenseInfo(
|
||||
string Name,
|
||||
string? Version,
|
||||
ImmutableArray<string> Expressions,
|
||||
ImmutableArray<RustLicenseFileReference> Files,
|
||||
string CargoTomlRelativePath);
|
||||
|
||||
internal sealed record RustLicenseFileReference(string RelativePath, string? Sha256);
|
||||
|
||||
Reference in New Issue
Block a user