Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.EntryTrace/Oci/OciImageConfig.cs
2025-10-28 15:10:40 +02:00

130 lines
4.1 KiB
C#

using System.Collections.Immutable;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.EntryTrace;
/// <summary>
/// Represents the deserialized OCI image config document.
/// </summary>
internal sealed class OciImageConfiguration
{
[JsonPropertyName("config")]
public OciImageConfig? Config { get; init; }
[JsonPropertyName("container_config")]
public OciImageConfig? ContainerConfig { get; init; }
}
/// <summary>
/// Logical representation of the OCI image config fields used by EntryTrace.
/// </summary>
public sealed class OciImageConfig
{
[JsonPropertyName("Env")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray<string> Environment { get; init; } = ImmutableArray<string>.Empty;
[JsonPropertyName("Entrypoint")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray<string> Entrypoint { get; init; } = ImmutableArray<string>.Empty;
[JsonPropertyName("Cmd")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray<string> Command { get; init; } = ImmutableArray<string>.Empty;
[JsonPropertyName("WorkingDir")]
public string? WorkingDirectory { get; init; }
[JsonPropertyName("User")]
public string? User { get; init; }
}
/// <summary>
/// Loads <see cref="OciImageConfig"/> instances from OCI config JSON.
/// </summary>
public static class OciImageConfigLoader
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true
};
public static OciImageConfig Load(string filePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
using var stream = File.OpenRead(filePath);
return Load(stream);
}
public static OciImageConfig Load(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);
var configuration = JsonSerializer.Deserialize<OciImageConfiguration>(stream, SerializerOptions)
?? throw new InvalidDataException("OCI image config is empty or invalid.");
if (configuration.Config is not null)
{
return configuration.Config;
}
if (configuration.ContainerConfig is not null)
{
return configuration.ContainerConfig;
}
throw new InvalidDataException("OCI image config does not include a config section.");
}
}
internal sealed class FlexibleStringListConverter : JsonConverter<ImmutableArray<string>>
{
public override ImmutableArray<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return ImmutableArray<string>.Empty;
}
if (reader.TokenType == JsonTokenType.StartArray)
{
var builder = ImmutableArray.CreateBuilder<string>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return builder.ToImmutable();
}
if (reader.TokenType == JsonTokenType.String)
{
builder.Add(reader.GetString() ?? string.Empty);
continue;
}
throw new JsonException($"Expected string elements in array but found {reader.TokenType}.");
}
}
if (reader.TokenType == JsonTokenType.String)
{
return ImmutableArray.Create(reader.GetString() ?? string.Empty);
}
throw new JsonException($"Unsupported JSON token {reader.TokenType} for string array.");
}
public override void Write(Utf8JsonWriter writer, ImmutableArray<string> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var entry in value)
{
writer.WriteStringValue(entry);
}
writer.WriteEndArray();
}
}