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(); } }