Restructure solution layout by module
This commit is contained in:
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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 '_';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user