Some checks are pending
Docs CI / lint-and-preview (push) Waiting to run
- Implemented tests for RancherHubConnector to validate fetching documents, handling errors, and managing state. - Added tests for CsafExporter to ensure deterministic serialization of CSAF documents. - Created tests for CycloneDX exporters and reconciler to verify correct handling of VEX claims and output structure. - Developed OpenVEX exporter tests to confirm the generation of canonical OpenVEX documents and statement merging logic. - Introduced Rust file caching and license scanning functionality, including a cache key structure and hash computation. - Added sample Cargo.toml and LICENSE files for testing Rust license scanning functionality.
313 lines
8.8 KiB
C#
313 lines
8.8 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Collections.Immutable;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal;
|
|
|
|
internal static class RustCargoLockParser
|
|
{
|
|
private static readonly ConcurrentDictionary<RustFileCacheKey, ImmutableArray<RustCargoPackage>> Cache = new();
|
|
|
|
public static IReadOnlyList<RustCargoPackage> Parse(string path, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
throw new ArgumentException("Lock path is required", nameof(path));
|
|
}
|
|
|
|
if (!RustFileCacheKey.TryCreate(path, out var key))
|
|
{
|
|
return Array.Empty<RustCargoPackage>();
|
|
}
|
|
|
|
var packages = Cache.GetOrAdd(
|
|
key,
|
|
static (_, state) => ParseInternal(state.Path, state.CancellationToken),
|
|
(Path: path, CancellationToken: cancellationToken));
|
|
|
|
return packages.IsDefaultOrEmpty ? Array.Empty<RustCargoPackage>() : packages;
|
|
}
|
|
|
|
private static ImmutableArray<RustCargoPackage> ParseInternal(string path, CancellationToken cancellationToken)
|
|
{
|
|
var resultBuilder = ImmutableArray.CreateBuilder<RustCargoPackage>();
|
|
|
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
using var reader = new StreamReader(stream);
|
|
RustCargoPackageBuilder? packageBuilder = null;
|
|
string? currentArrayKey = null;
|
|
var arrayValues = new List<string>();
|
|
|
|
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(packageBuilder, resultBuilder);
|
|
packageBuilder = new RustCargoPackageBuilder();
|
|
currentArrayKey = null;
|
|
arrayValues.Clear();
|
|
continue;
|
|
}
|
|
|
|
if (packageBuilder 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
packageBuilder.SetArray(currentArrayKey, arrayValues);
|
|
currentArrayKey = null;
|
|
arrayValues.Clear();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var parsed = ExtractString(valuePart);
|
|
if (parsed is not null)
|
|
{
|
|
packageBuilder.SetField(key, parsed);
|
|
}
|
|
}
|
|
|
|
if (currentArrayKey is not null && arrayValues.Count > 0)
|
|
{
|
|
packageBuilder?.SetArray(currentArrayKey, arrayValues);
|
|
}
|
|
|
|
FlushCurrent(packageBuilder, resultBuilder);
|
|
return resultBuilder.ToImmutable();
|
|
}
|
|
|
|
private static ReadOnlySpan<char> TrimComments(ReadOnlySpan<char> line)
|
|
{
|
|
var index = line.IndexOf('#');
|
|
if (index >= 0)
|
|
{
|
|
line = line[..index];
|
|
}
|
|
|
|
return line.Trim();
|
|
}
|
|
|
|
private static bool IsPackageHeader(ReadOnlySpan<char> value)
|
|
=> value.SequenceEqual("[[package]]".AsSpan());
|
|
|
|
private static IEnumerable<string> 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<char> 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? packageBuilder, ImmutableArray<RustCargoPackage>.Builder packages)
|
|
{
|
|
if (packageBuilder is null || !packageBuilder.HasData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (packageBuilder.TryBuild(out var package))
|
|
{
|
|
packages.Add(package);
|
|
}
|
|
}
|
|
|
|
private sealed class RustCargoPackageBuilder
|
|
{
|
|
private readonly SortedSet<string> _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<char> 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<string> 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<string> Dependencies);
|