using System.Collections.Immutable;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.EntryTrace;
///
/// Represents the deserialized OCI image config document.
///
internal sealed class OciImageConfiguration
{
[JsonPropertyName("config")]
public OciImageConfig? Config { get; init; }
[JsonPropertyName("container_config")]
public OciImageConfig? ContainerConfig { get; init; }
}
///
/// Logical representation of the OCI image config fields used by EntryTrace.
///
public sealed class OciImageConfig
{
[JsonPropertyName("Env")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray Environment { get; init; } = ImmutableArray.Empty;
[JsonPropertyName("Entrypoint")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray Entrypoint { get; init; } = ImmutableArray.Empty;
[JsonPropertyName("Cmd")]
[JsonConverter(typeof(FlexibleStringListConverter))]
public ImmutableArray Command { get; init; } = ImmutableArray.Empty;
[JsonPropertyName("WorkingDir")]
public string? WorkingDirectory { get; init; }
[JsonPropertyName("User")]
public string? User { get; init; }
}
///
/// Loads instances from OCI config JSON.
///
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(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>
{
public override ImmutableArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return ImmutableArray.Empty;
}
if (reader.TokenType == JsonTokenType.StartArray)
{
var builder = ImmutableArray.CreateBuilder();
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 value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var entry in value)
{
writer.WriteStringValue(entry);
}
writer.WriteEndArray();
}
}