namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal; internal static class RustCargoLockParser { public static IReadOnlyList Parse(string path, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("Lock path is required", nameof(path)); } var info = new FileInfo(path); if (!info.Exists) { return Array.Empty(); } var packages = new List(); using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using var reader = new StreamReader(stream); RustCargoPackageBuilder? builder = null; string? currentArrayKey = null; var arrayValues = new List(); while (!reader.EndOfStream) { cancellationToken.ThrowIfCancellationRequested(); var line = reader.ReadLine(); if (line is null) { break; } var trimmed = TrimComments(line.AsSpan()); if (trimmed.Length == 0) { continue; } if (IsPackageHeader(trimmed)) { FlushCurrent(builder, packages); builder = new RustCargoPackageBuilder(); currentArrayKey = null; arrayValues.Clear(); continue; } if (builder is null) { continue; } if (currentArrayKey is not null) { if (trimmed[0] == ']') { builder.SetArray(currentArrayKey, arrayValues); currentArrayKey = null; arrayValues.Clear(); continue; } var value = ExtractString(trimmed); if (!string.IsNullOrEmpty(value)) { arrayValues.Add(value); } continue; } if (trimmed[0] == '[') { // Entering a new table; finish any pending package and skip section. FlushCurrent(builder, packages); builder = null; continue; } var equalsIndex = trimmed.IndexOf('='); if (equalsIndex < 0) { continue; } var key = trimmed[..equalsIndex].Trim(); var valuePart = trimmed[(equalsIndex + 1)..].Trim(); if (valuePart.Length == 0) { continue; } if (valuePart[0] == '[') { currentArrayKey = key.ToString(); arrayValues.Clear(); if (valuePart.Length > 1 && valuePart[^1] == ']') { var inline = valuePart[1..^1].Trim(); if (inline.Length > 0) { foreach (var token in SplitInlineArray(inline.ToString())) { var parsedValue = ExtractString(token.AsSpan()); if (!string.IsNullOrEmpty(parsedValue)) { arrayValues.Add(parsedValue); } } } builder.SetArray(currentArrayKey, arrayValues); currentArrayKey = null; arrayValues.Clear(); } continue; } var parsed = ExtractString(valuePart); if (parsed is not null) { builder.SetField(key, parsed); } } if (currentArrayKey is not null && arrayValues.Count > 0) { builder?.SetArray(currentArrayKey, arrayValues); } FlushCurrent(builder, packages); return packages; } private static ReadOnlySpan TrimComments(ReadOnlySpan line) { var index = line.IndexOf('#'); if (index >= 0) { line = line[..index]; } return line.Trim(); } private static bool IsPackageHeader(ReadOnlySpan value) => value.SequenceEqual("[[package]]".AsSpan()); private static IEnumerable SplitInlineArray(string value) { var start = 0; var inString = false; for (var i = 0; i < value.Length; i++) { var current = value[i]; if (current == '"') { inString = !inString; } if (current == ',' && !inString) { var item = value.AsSpan(start, i - start).Trim(); if (item.Length > 0) { yield return item.ToString(); } start = i + 1; } } if (start < value.Length) { var item = value.AsSpan(start).Trim(); if (item.Length > 0) { yield return item.ToString(); } } } private static string? ExtractString(ReadOnlySpan value) { if (value.Length == 0) { return null; } if (value[0] == '"' && value[^1] == '"') { var inner = value[1..^1]; return inner.ToString(); } var trimmed = value.Trim(); return trimmed.Length == 0 ? null : trimmed.ToString(); } private static void FlushCurrent(RustCargoPackageBuilder? builder, List packages) { if (builder is null || !builder.HasData) { return; } if (builder.TryBuild(out var package)) { packages.Add(package); } } private sealed class RustCargoPackageBuilder { private readonly SortedSet _dependencies = new(StringComparer.Ordinal); private string? _name; private string? _version; private string? _source; private string? _checksum; public bool HasData => !string.IsNullOrWhiteSpace(_name); public void SetField(ReadOnlySpan key, string value) { if (key.SequenceEqual("name".AsSpan())) { _name ??= value.Trim(); } else if (key.SequenceEqual("version".AsSpan())) { _version ??= value.Trim(); } else if (key.SequenceEqual("source".AsSpan())) { _source ??= value.Trim(); } else if (key.SequenceEqual("checksum".AsSpan())) { _checksum ??= value.Trim(); } } public void SetArray(string key, IEnumerable values) { if (!string.Equals(key, "dependencies", StringComparison.Ordinal)) { return; } foreach (var entry in values) { if (string.IsNullOrWhiteSpace(entry)) { continue; } var normalized = entry.Trim(); if (normalized.Length > 0) { _dependencies.Add(normalized); } } } public bool TryBuild(out RustCargoPackage package) { if (string.IsNullOrWhiteSpace(_name)) { package = null!; return false; } package = new RustCargoPackage( _name!, _version ?? string.Empty, _source, _checksum, _dependencies.ToArray()); return true; } } } internal sealed record RustCargoPackage( string Name, string Version, string? Source, string? Checksum, IReadOnlyList Dependencies);