using System.Collections.Concurrent; using System.Text.Json; namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal; internal static class RustFingerprintScanner { private static readonly EnumerationOptions Enumeration = new() { MatchCasing = MatchCasing.CaseSensitive, IgnoreInaccessible = true, RecurseSubdirectories = true, AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint, }; private static readonly string FingerprintSegment = $"{Path.DirectorySeparatorChar}.fingerprint{Path.DirectorySeparatorChar}"; private static readonly ConcurrentDictionary Cache = new(); public static IReadOnlyList Scan(string rootPath, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(rootPath)) { throw new ArgumentException("Root path is required", nameof(rootPath)); } var results = new List(); foreach (var path in Directory.EnumerateFiles(rootPath, "*.json", Enumeration)) { cancellationToken.ThrowIfCancellationRequested(); if (!path.Contains(FingerprintSegment, StringComparison.Ordinal)) { continue; } if (!RustFileCacheKey.TryCreate(path, out var key)) { continue; } var record = Cache.GetOrAdd( key, static (_, state) => ParseFingerprint(state), path); if (record is not null) { results.Add(record); } } return results; } private static RustFingerprintRecord? ParseFingerprint(string path) { try { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using var document = JsonDocument.Parse(stream); var root = document.RootElement; var pkgId = TryGetString(root, "pkgid") ?? TryGetString(root, "package_id") ?? TryGetString(root, "packageId"); var (name, version, source) = ParseIdentity(pkgId, path); if (string.IsNullOrWhiteSpace(name)) { return null; } var profile = TryGetString(root, "profile"); var targetKind = TryGetKind(root); return new RustFingerprintRecord( Name: name!, Version: version, Source: source, TargetKind: targetKind, Profile: profile, AbsolutePath: path); } catch (JsonException) { return null; } catch (IOException) { return null; } catch (UnauthorizedAccessException) { return null; } } private static (string? Name, string? Version, string? Source) ParseIdentity(string? pkgId, string filePath) { if (!string.IsNullOrWhiteSpace(pkgId)) { var span = pkgId.AsSpan().Trim(); var firstSpace = span.IndexOf(' '); if (firstSpace > 0 && firstSpace < span.Length - 1) { var name = span[..firstSpace].ToString(); var remaining = span[(firstSpace + 1)..].Trim(); var secondSpace = remaining.IndexOf(' '); if (secondSpace < 0) { return (name, remaining.ToString(), null); } var version = remaining[..secondSpace].ToString(); var potentialSource = remaining[(secondSpace + 1)..].Trim(); if (potentialSource.Length > 1 && potentialSource[0] == '(' && potentialSource[^1] == ')') { potentialSource = potentialSource[1..^1].Trim(); } var source = potentialSource.Length == 0 ? null : potentialSource.ToString(); return (name, version, source); } } var directory = Path.GetDirectoryName(filePath); if (string.IsNullOrEmpty(directory)) { return (null, null, null); } var crateDirectory = Path.GetFileName(directory); if (string.IsNullOrWhiteSpace(crateDirectory)) { return (null, null, null); } var dashIndex = crateDirectory.LastIndexOf('-'); if (dashIndex <= 0) { return (crateDirectory, null, null); } var maybeName = crateDirectory[..dashIndex]; return (maybeName, null, null); } private static string? TryGetKind(JsonElement root) { if (root.TryGetProperty("target_kind", out var array) && array.ValueKind == JsonValueKind.Array && array.GetArrayLength() > 0) { var first = array[0]; if (first.ValueKind == JsonValueKind.String) { return first.GetString(); } } if (root.TryGetProperty("target", out var target) && target.ValueKind == JsonValueKind.String) { return target.GetString(); } return null; } private static string? TryGetString(JsonElement element, string propertyName) { if (element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String) { return value.GetString(); } return null; } } internal sealed record RustFingerprintRecord( string Name, string? Version, string? Source, string? TargetKind, string? Profile, string AbsolutePath);