using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; namespace StellaOps.Feedser.Source.Distro.Debian.Internal; internal static class DebianListParser { private static readonly Regex HeaderRegex = new("^\\[(?[^\\]]+)\\]\\s+(?DSA-\\d{4,}-\\d+)\\s+(?.+)$", RegexOptions.Compiled); private static readonly Regex CveRegex = new("CVE-\\d{4}-\\d{3,7}", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static IReadOnlyList<DebianListEntry> Parse(string? content) { if (string.IsNullOrWhiteSpace(content)) { return Array.Empty<DebianListEntry>(); } var entries = new List<DebianListEntry>(); var currentCves = new HashSet<string>(StringComparer.OrdinalIgnoreCase); DateTimeOffset currentDate = default; string? currentId = null; string? currentTitle = null; string? currentPackage = null; foreach (var rawLine in content.Split('\n')) { var line = rawLine.TrimEnd('\r'); if (string.IsNullOrWhiteSpace(line)) { continue; } if (line[0] == '[') { if (currentId is not null && currentTitle is not null && currentPackage is not null) { entries.Add(new DebianListEntry( currentId, currentDate, currentTitle, currentPackage, currentCves.Count == 0 ? Array.Empty<string>() : new List<string>(currentCves))); } currentCves.Clear(); currentId = null; currentTitle = null; currentPackage = null; var match = HeaderRegex.Match(line); if (!match.Success) { continue; } if (!DateTimeOffset.TryParseExact( match.Groups["date"].Value, new[] { "dd MMM yyyy", "d MMM yyyy" }, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out currentDate)) { continue; } currentId = match.Groups["id"].Value.Trim(); currentTitle = match.Groups["title"].Value.Trim(); var separatorIndex = currentTitle.IndexOf(" - ", StringComparison.Ordinal); currentPackage = separatorIndex > 0 ? currentTitle[..separatorIndex].Trim() : currentTitle.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); if (string.IsNullOrWhiteSpace(currentPackage)) { currentPackage = currentId; } continue; } if (line[0] == '{') { foreach (Match match in CveRegex.Matches(line)) { if (match.Success && !string.IsNullOrWhiteSpace(match.Value)) { currentCves.Add(match.Value.ToUpperInvariant()); } } } } if (currentId is not null && currentTitle is not null && currentPackage is not null) { entries.Add(new DebianListEntry( currentId, currentDate, currentTitle, currentPackage, currentCves.Count == 0 ? Array.Empty<string>() : new List<string>(currentCves))); } return entries; } }