Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoAnalyzerMetrics
{
private static readonly Meter Meter = new("StellaOps.Scanner.Analyzers.Lang.Go", "1.0.0");
private static readonly Counter<long> HeuristicCounter = Meter.CreateCounter<long>(
"scanner_analyzer_golang_heuristic_total",
unit: "components",
description: "Counts Go components emitted via heuristic fallbacks when build metadata is missing.");
public static void RecordHeuristic(GoStrippedBinaryIndicator indicator, bool hasVersionHint)
{
HeuristicCounter.Add(
1,
new KeyValuePair<string, object?>("indicator", NormalizeIndicator(indicator)),
new KeyValuePair<string, object?>("version_hint", hasVersionHint ? "present" : "absent"));
}
private static string NormalizeIndicator(GoStrippedBinaryIndicator indicator)
=> indicator switch
{
GoStrippedBinaryIndicator.BuildId => "build-id",
GoStrippedBinaryIndicator.GoRuntimeMarkers => "runtime-markers",
_ => "unknown",
};
}

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using System.IO;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoBinaryScanner
{
private static readonly ReadOnlyMemory<byte> BuildInfoMagic = new byte[]
{
0xFF, (byte)' ', (byte)'G', (byte)'o', (byte)' ', (byte)'b', (byte)'u', (byte)'i', (byte)'l', (byte)'d', (byte)'i', (byte)'n', (byte)'f', (byte)':'
};
private static readonly ReadOnlyMemory<byte> BuildIdMarker = Encoding.ASCII.GetBytes("Go build ID:");
private static readonly ReadOnlyMemory<byte> GoPclnTabMarker = Encoding.ASCII.GetBytes(".gopclntab");
private static readonly ReadOnlyMemory<byte> GoVersionPrefix = Encoding.ASCII.GetBytes("go1.");
public static IEnumerable<string> EnumerateCandidateFiles(string rootPath)
{
var enumeration = new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
MatchCasing = MatchCasing.CaseSensitive,
};
foreach (var path in Directory.EnumerateFiles(rootPath, "*", enumeration))
{
yield return path;
}
}
public static bool TryReadBuildInfo(string filePath, out string? goVersion, out string? moduleData)
{
goVersion = null;
moduleData = null;
FileInfo info;
try
{
info = new FileInfo(filePath);
if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024)
{
return false;
}
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (System.Security.SecurityException)
{
return false;
}
var length = info.Length;
if (length <= 0)
{
return false;
}
var inspectLength = (int)Math.Min(length, int.MaxValue);
var buffer = ArrayPool<byte>.Shared.Rent(inspectLength);
try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var totalRead = 0;
while (totalRead < inspectLength)
{
var read = stream.Read(buffer, totalRead, inspectLength - totalRead);
if (read <= 0)
{
break;
}
totalRead += read;
}
if (totalRead < 64)
{
return false;
}
var span = new ReadOnlySpan<byte>(buffer, 0, totalRead);
var offset = span.IndexOf(BuildInfoMagic.Span);
if (offset < 0)
{
return false;
}
var view = span[offset..];
return GoBuildInfoDecoder.TryDecode(view, out goVersion, out moduleData);
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
finally
{
Array.Clear(buffer, 0, inspectLength);
ArrayPool<byte>.Shared.Return(buffer);
}
}
public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification)
{
classification = default;
FileInfo fileInfo;
try
{
fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
return false;
}
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (System.Security.SecurityException)
{
return false;
}
var length = fileInfo.Length;
if (length < 128)
{
return false;
}
const int WindowSize = 128 * 1024;
var readSize = (int)Math.Min(length, WindowSize);
var buffer = ArrayPool<byte>.Shared.Rent(readSize);
try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var headRead = stream.Read(buffer, 0, readSize);
if (headRead <= 0)
{
return false;
}
var headSpan = new ReadOnlySpan<byte>(buffer, 0, headRead);
var hasBuildId = headSpan.IndexOf(BuildIdMarker.Span) >= 0;
var hasPcln = headSpan.IndexOf(GoPclnTabMarker.Span) >= 0;
var goVersion = ExtractGoVersion(headSpan);
if (length > headRead)
{
var tailSize = Math.Min(readSize, (int)length);
if (tailSize > 0)
{
stream.Seek(-tailSize, SeekOrigin.End);
var tailRead = stream.Read(buffer, 0, tailSize);
if (tailRead > 0)
{
var tailSpan = new ReadOnlySpan<byte>(buffer, 0, tailRead);
hasBuildId |= tailSpan.IndexOf(BuildIdMarker.Span) >= 0;
hasPcln |= tailSpan.IndexOf(GoPclnTabMarker.Span) >= 0;
goVersion ??= ExtractGoVersion(tailSpan);
}
}
}
if (hasBuildId)
{
classification = new GoStrippedBinaryClassification(
filePath,
GoStrippedBinaryIndicator.BuildId,
goVersion);
return true;
}
if (hasPcln && !string.IsNullOrEmpty(goVersion))
{
classification = new GoStrippedBinaryClassification(
filePath,
GoStrippedBinaryIndicator.GoRuntimeMarkers,
goVersion);
return true;
}
return false;
}
finally
{
Array.Clear(buffer, 0, readSize);
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static string? ExtractGoVersion(ReadOnlySpan<byte> data)
{
var prefix = GoVersionPrefix.Span;
var span = data;
while (!span.IsEmpty)
{
var index = span.IndexOf(prefix);
if (index < 0)
{
return null;
}
var absoluteIndex = data.Length - span.Length + index;
if (absoluteIndex > 0)
{
var previous = (char)data[absoluteIndex - 1];
if (char.IsLetterOrDigit(previous))
{
span = span[(index + 1)..];
continue;
}
}
var start = absoluteIndex;
var end = start + prefix.Length;
while (end < data.Length && IsVersionCharacter((char)data[end]))
{
end++;
}
if (end - start <= prefix.Length)
{
span = span[(index + 1)..];
continue;
}
var candidate = data[start..end];
return Encoding.ASCII.GetString(candidate);
}
return null;
}
private static bool IsVersionCharacter(char value)
=> (value >= '0' && value <= '9')
|| (value >= 'a' && value <= 'z')
|| (value >= 'A' && value <= 'Z')
|| value is '.' or '-' or '+' or '_';
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal sealed class GoBuildInfo
{
public GoBuildInfo(
string goVersion,
string absoluteBinaryPath,
string modulePath,
GoModule mainModule,
IEnumerable<GoModule> dependencies,
IEnumerable<KeyValuePair<string, string?>> settings,
GoDwarfMetadata? dwarfMetadata = null)
: this(
goVersion,
absoluteBinaryPath,
modulePath,
mainModule,
dependencies?
.Where(static module => module is not null)
.ToImmutableArray()
?? ImmutableArray<GoModule>.Empty,
settings?
.Where(static pair => pair.Key is not null)
.Select(static pair => new KeyValuePair<string, string?>(pair.Key, pair.Value))
.ToImmutableArray()
?? ImmutableArray<KeyValuePair<string, string?>>.Empty,
dwarfMetadata)
{
}
private GoBuildInfo(
string goVersion,
string absoluteBinaryPath,
string modulePath,
GoModule mainModule,
ImmutableArray<GoModule> dependencies,
ImmutableArray<KeyValuePair<string, string?>> settings,
GoDwarfMetadata? dwarfMetadata)
{
GoVersion = goVersion ?? throw new ArgumentNullException(nameof(goVersion));
AbsoluteBinaryPath = absoluteBinaryPath ?? throw new ArgumentNullException(nameof(absoluteBinaryPath));
ModulePath = modulePath ?? throw new ArgumentNullException(nameof(modulePath));
MainModule = mainModule ?? throw new ArgumentNullException(nameof(mainModule));
Dependencies = dependencies;
Settings = settings;
DwarfMetadata = dwarfMetadata;
}
public string GoVersion { get; }
public string AbsoluteBinaryPath { get; }
public string ModulePath { get; }
public GoModule MainModule { get; }
public ImmutableArray<GoModule> Dependencies { get; }
public ImmutableArray<KeyValuePair<string, string?>> Settings { get; }
public GoDwarfMetadata? DwarfMetadata { get; }
public GoBuildInfo WithDwarf(GoDwarfMetadata metadata)
{
ArgumentNullException.ThrowIfNull(metadata);
return new GoBuildInfo(
GoVersion,
AbsoluteBinaryPath,
ModulePath,
MainModule,
Dependencies,
Settings,
metadata);
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoBuildInfoDecoder
{
private const string BuildInfoMagic = "\xff Go buildinf:";
private const int HeaderSize = 32;
private const byte VarintEncodingFlag = 0x02;
public static bool TryDecode(ReadOnlySpan<byte> data, out string? goVersion, out string? moduleData)
{
goVersion = null;
moduleData = null;
if (data.Length < HeaderSize)
{
return false;
}
if (!IsMagicMatch(data))
{
return false;
}
var pointerSize = data[14];
var flags = data[15];
if (pointerSize != 4 && pointerSize != 8)
{
return false;
}
if ((flags & VarintEncodingFlag) == 0)
{
// Older Go toolchains encode pointers to strings instead of inline data.
// The Sprint 10 scope targets Go 1.18+, which always sets the varint flag.
return false;
}
var payload = data.Slice(HeaderSize);
if (!TryReadVarString(payload, out var version, out var consumed))
{
return false;
}
payload = payload.Slice(consumed);
if (!TryReadVarString(payload, out var modules, out _))
{
return false;
}
if (string.IsNullOrWhiteSpace(version))
{
return false;
}
modules = StripSentinel(modules);
goVersion = version;
moduleData = modules;
return !string.IsNullOrWhiteSpace(moduleData);
}
private static bool IsMagicMatch(ReadOnlySpan<byte> data)
{
if (data.Length < BuildInfoMagic.Length)
{
return false;
}
for (var i = 0; i < BuildInfoMagic.Length; i++)
{
if (data[i] != BuildInfoMagic[i])
{
return false;
}
}
return true;
}
private static bool TryReadVarString(ReadOnlySpan<byte> data, out string result, out int consumed)
{
result = string.Empty;
consumed = 0;
if (!TryReadUVarint(data, out var length, out var lengthBytes))
{
return false;
}
if (length > int.MaxValue)
{
return false;
}
var stringLength = (int)length;
var totalRequired = lengthBytes + stringLength;
if (stringLength <= 0 || totalRequired > data.Length)
{
return false;
}
var slice = data.Slice(lengthBytes, stringLength);
result = Encoding.UTF8.GetString(slice);
consumed = totalRequired;
return true;
}
private static bool TryReadUVarint(ReadOnlySpan<byte> data, out ulong value, out int bytesRead)
{
value = 0;
bytesRead = 0;
ulong x = 0;
var shift = 0;
for (var i = 0; i < data.Length; i++)
{
var b = data[i];
if (b < 0x80)
{
if (i > 9 || i == 9 && b > 1)
{
return false;
}
value = x | (ulong)b << shift;
bytesRead = i + 1;
return true;
}
x |= (ulong)(b & 0x7F) << shift;
shift += 7;
}
return false;
}
private static string StripSentinel(string value)
{
if (string.IsNullOrEmpty(value) || value.Length < 33)
{
return value;
}
var sentinelIndex = value.Length - 17;
if (value[sentinelIndex] != '\n')
{
return value;
}
return value[16..^16];
}
}

View File

@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoBuildInfoParser
{
private const string PathPrefix = "path\t";
private const string ModulePrefix = "mod\t";
private const string DependencyPrefix = "dep\t";
private const string ReplacementPrefix = "=>\t";
private const string BuildPrefix = "build\t";
public static bool TryParse(string goVersion, string absoluteBinaryPath, string rawModuleData, out GoBuildInfo? info)
{
info = null;
if (string.IsNullOrWhiteSpace(goVersion) || string.IsNullOrWhiteSpace(rawModuleData))
{
return false;
}
string? modulePath = null;
GoModule? mainModule = null;
var dependencies = new List<GoModule>();
var settings = new SortedDictionary<string, string?>(StringComparer.Ordinal);
GoModule? lastModule = null;
using var reader = new StringReader(rawModuleData);
while (reader.ReadLine() is { } line)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (line.StartsWith(PathPrefix, StringComparison.Ordinal))
{
modulePath = line[PathPrefix.Length..].Trim();
continue;
}
if (line.StartsWith(ModulePrefix, StringComparison.Ordinal))
{
mainModule = ParseModule(line.AsSpan(ModulePrefix.Length), isMain: true);
lastModule = mainModule;
continue;
}
if (line.StartsWith(DependencyPrefix, StringComparison.Ordinal))
{
var dependency = ParseModule(line.AsSpan(DependencyPrefix.Length), isMain: false);
if (dependency is not null)
{
dependencies.Add(dependency);
lastModule = dependency;
}
continue;
}
if (line.StartsWith(ReplacementPrefix, StringComparison.Ordinal))
{
if (lastModule is null)
{
continue;
}
var replacement = ParseReplacement(line.AsSpan(ReplacementPrefix.Length));
if (replacement is not null)
{
lastModule.SetReplacement(replacement);
}
continue;
}
if (line.StartsWith(BuildPrefix, StringComparison.Ordinal))
{
var pair = ParseBuildSetting(line.AsSpan(BuildPrefix.Length));
if (!string.IsNullOrEmpty(pair.Key))
{
settings[pair.Key] = pair.Value;
}
}
}
if (mainModule is null)
{
return false;
}
if (string.IsNullOrEmpty(modulePath))
{
modulePath = mainModule.Path;
}
info = new GoBuildInfo(
goVersion,
absoluteBinaryPath,
modulePath,
mainModule,
dependencies,
settings);
return true;
}
private static GoModule? ParseModule(ReadOnlySpan<char> span, bool isMain)
{
var fields = SplitFields(span, expected: 4);
if (fields.Count == 0)
{
return null;
}
var path = fields[0];
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
var version = fields.Count > 1 ? fields[1] : null;
var sum = fields.Count > 2 ? fields[2] : null;
return new GoModule(path, version, sum, isMain);
}
private static GoModuleReplacement? ParseReplacement(ReadOnlySpan<char> span)
{
var fields = SplitFields(span, expected: 3);
if (fields.Count == 0)
{
return null;
}
var path = fields[0];
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
var version = fields.Count > 1 ? fields[1] : null;
var sum = fields.Count > 2 ? fields[2] : null;
return new GoModuleReplacement(path, version, sum);
}
private static KeyValuePair<string, string?> ParseBuildSetting(ReadOnlySpan<char> span)
{
span = span.Trim();
if (span.IsEmpty)
{
return default;
}
var separatorIndex = span.IndexOf('=');
if (separatorIndex <= 0)
{
return default;
}
var rawKey = span[..separatorIndex].Trim();
var rawValue = span[(separatorIndex + 1)..].Trim();
var key = Unquote(rawKey.ToString());
if (string.IsNullOrWhiteSpace(key))
{
return default;
}
var value = Unquote(rawValue.ToString());
return new KeyValuePair<string, string?>(key, value);
}
private static List<string> SplitFields(ReadOnlySpan<char> span, int expected)
{
var fields = new List<string>(expected);
var builder = new StringBuilder();
for (var i = 0; i < span.Length; i++)
{
var current = span[i];
if (current == '\t')
{
fields.Add(builder.ToString());
builder.Clear();
continue;
}
builder.Append(current);
}
fields.Add(builder.ToString());
return fields;
}
private static string Unquote(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
value = value.Trim();
if (value.Length < 2)
{
return value;
}
if (value[0] == '"' && value[^1] == '"')
{
try
{
return JsonSerializer.Deserialize<string>(value) ?? value;
}
catch (JsonException)
{
return value;
}
}
if (value[0] == '`' && value[^1] == '`')
{
return value[1..^1];
}
return value;
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Security;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoBuildInfoProvider
{
private static readonly ConcurrentDictionary<GoBinaryCacheKey, GoBuildInfo?> Cache = new();
public static bool TryGetBuildInfo(string absolutePath, out GoBuildInfo? info)
{
info = null;
FileInfo fileInfo;
try
{
fileInfo = new FileInfo(absolutePath);
if (!fileInfo.Exists)
{
return false;
}
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (System.Security.SecurityException)
{
return false;
}
var key = new GoBinaryCacheKey(absolutePath, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks);
info = Cache.GetOrAdd(key, static (cacheKey, path) => CreateBuildInfo(path), absolutePath);
return info is not null;
}
private static GoBuildInfo? CreateBuildInfo(string absolutePath)
{
if (!GoBinaryScanner.TryReadBuildInfo(absolutePath, out var goVersion, out var moduleData))
{
return null;
}
if (string.IsNullOrWhiteSpace(goVersion) || string.IsNullOrWhiteSpace(moduleData))
{
return null;
}
if (!GoBuildInfoParser.TryParse(goVersion!, absolutePath, moduleData!, out var buildInfo) || buildInfo is null)
{
return null;
}
if (GoDwarfReader.TryRead(absolutePath, out var dwarf) && dwarf is not null)
{
buildInfo = buildInfo.WithDwarf(dwarf);
}
return buildInfo;
}
private readonly record struct GoBinaryCacheKey(string Path, long Length, long LastWriteTicks)
{
private readonly string _normalizedPath = OperatingSystem.IsWindows()
? Path.ToLowerInvariant()
: Path;
public bool Equals(GoBinaryCacheKey other)
=> Length == other.Length
&& LastWriteTicks == other.LastWriteTicks
&& string.Equals(_normalizedPath, other._normalizedPath, StringComparison.Ordinal);
public override int GetHashCode()
=> HashCode.Combine(_normalizedPath, Length, LastWriteTicks);
}
}

View File

@@ -0,0 +1,33 @@
using System;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal sealed class GoDwarfMetadata
{
public GoDwarfMetadata(string? vcsSystem, string? revision, bool? modified, string? timestampUtc)
{
VcsSystem = Normalize(vcsSystem);
Revision = Normalize(revision);
Modified = modified;
TimestampUtc = Normalize(timestampUtc);
}
public string? VcsSystem { get; }
public string? Revision { get; }
public bool? Modified { get; }
public string? TimestampUtc { get; }
private static string? Normalize(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
return trimmed.Length == 0 ? null : trimmed;
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Buffers;
using System.IO;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoDwarfReader
{
private static readonly byte[] VcsSystemToken = Encoding.UTF8.GetBytes("vcs=");
private static readonly byte[] VcsRevisionToken = Encoding.UTF8.GetBytes("vcs.revision=");
private static readonly byte[] VcsModifiedToken = Encoding.UTF8.GetBytes("vcs.modified=");
private static readonly byte[] VcsTimeToken = Encoding.UTF8.GetBytes("vcs.time=");
public static bool TryRead(string path, out GoDwarfMetadata? metadata)
{
metadata = null;
FileInfo fileInfo;
try
{
fileInfo = new FileInfo(path);
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
{
return false;
}
var length = fileInfo.Length;
var readLength = (int)Math.Min(length, int.MaxValue);
var buffer = ArrayPool<byte>.Shared.Rent(readLength);
var bytesRead = 0;
try
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
bytesRead = stream.Read(buffer, 0, readLength);
if (bytesRead <= 0)
{
return false;
}
var data = new ReadOnlySpan<byte>(buffer, 0, bytesRead);
var revision = ExtractValue(data, VcsRevisionToken);
var modifiedText = ExtractValue(data, VcsModifiedToken);
var timestamp = ExtractValue(data, VcsTimeToken);
var system = ExtractValue(data, VcsSystemToken);
bool? modified = null;
if (!string.IsNullOrWhiteSpace(modifiedText))
{
if (bool.TryParse(modifiedText, out var parsed))
{
modified = parsed;
}
}
if (string.IsNullOrWhiteSpace(revision) && string.IsNullOrWhiteSpace(system) && modified is null && string.IsNullOrWhiteSpace(timestamp))
{
return false;
}
metadata = new GoDwarfMetadata(system, revision, modified, timestamp);
return true;
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
finally
{
Array.Clear(buffer, 0, bytesRead);
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static string? ExtractValue(ReadOnlySpan<byte> data, ReadOnlySpan<byte> token)
{
var index = data.IndexOf(token);
if (index < 0)
{
return null;
}
var start = index + token.Length;
var end = start;
while (end < data.Length)
{
var current = data[end];
if (current == 0 || current == (byte)'\n' || current == (byte)'\r')
{
break;
}
end++;
}
if (end <= start)
{
return null;
}
return Encoding.UTF8.GetString(data.Slice(start, end - start));
}
}

View File

@@ -0,0 +1,67 @@
using System;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal sealed class GoModule
{
public GoModule(string path, string? version, string? sum, bool isMain)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Version = Normalize(version);
Sum = Normalize(sum);
IsMain = isMain;
}
public string Path { get; }
public string? Version { get; }
public string? Sum { get; }
public GoModuleReplacement? Replacement { get; private set; }
public bool IsMain { get; }
public void SetReplacement(GoModuleReplacement replacement)
{
Replacement = replacement ?? throw new ArgumentNullException(nameof(replacement));
}
private static string? Normalize(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
return trimmed.Length == 0 ? null : trimmed;
}
}
internal sealed class GoModuleReplacement
{
public GoModuleReplacement(string path, string? version, string? sum)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Version = Normalize(version);
Sum = Normalize(sum);
}
public string Path { get; }
public string? Version { get; }
public string? Sum { get; }
private static string? Normalize(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
return trimmed.Length == 0 ? null : trimmed;
}
}

View File

@@ -0,0 +1,13 @@
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal readonly record struct GoStrippedBinaryClassification(
string AbsolutePath,
GoStrippedBinaryIndicator Indicator,
string? GoVersionHint);
internal enum GoStrippedBinaryIndicator
{
None = 0,
BuildId,
GoRuntimeMarkers,
}