// TODO: Refactor to be more like Package code. This is messy // Substring should be replaced with a .split(": ") // Also Directly assign to the values instead of using temporary variables. using Serilog; using System.Text.Json.Serialization; namespace EdgeInstaller; sealed class ReleaseData { public DateTime Date { get; private set; } public List Architectures { get; private set; } public List PackageFiles { get; private set; } [JsonConstructor] public ReleaseData(DateTime date, List architectures, List packageFiles) { this.Date = date; this.Architectures = architectures; this.PackageFiles = packageFiles; } public ReleaseData(string Release) { Log.Information("Creating new ReleaseData object"); if (Release == "") { throw new ArgumentException( message: "Empty release provided to ReleaseData", paramName: "Release"); } Architectures = new(); PackageFiles = new(); // Pull release apart List ReleaseLines = Release.Split("\n", StringSplitOptions.RemoveEmptyEntries).ToList(); Log.Debug("ReleaseLines = {@releaselines}", ReleaseLines); // Safely find and parse the Date field string DateLine = ReleaseLines.Where( Line => Line.Contains("Date:")) .FirstOrDefault("").Substring(5); Log.Debug("DateLine = {DateLine}", DateLine); if (DateTime.TryParse(DateLine, out DateTime result)) { Log.Debug("Extracted date {@date}", result); Date = result; } else { throw _lineFormatException("date"); } // Safely find and add the possible architectures string ArchitecturesLine = ReleaseLines.Where( Line => Line.Contains("Architectures:")) .FirstOrDefault(""); Log.Debug("Arch line: {archline}", ArchitecturesLine); if (ArchitecturesLine == "") { throw _lineFormatException("architectures"); } Architectures = ArchitecturesLine .Substring(ArchitecturesLine.IndexOf(":") + 1) // We only want items after the label .Split(" ", StringSplitOptions.RemoveEmptyEntries) // Space delimited list .ToList(); // Convert to the superior collection type Log.Debug("Extracted architectures {@architectures}", Architectures); // Time to process the package files.. This is gonna be fun.. // WARNING: this code is a little hacky // scope for possible optimisation // First split the Lines to only get the file data List SHA256FileData = ReleaseLines.Where((value, index) => index > ReleaseLines.IndexOf("SHA256:") && index < ReleaseLines.IndexOf("SHA512:") ).ToList(); Log.Debug("SHA256FileData = {@data}", SHA256FileData); // Now process these lines to get the data we need List releasePackageFiles = new(); foreach (var fileData in SHA256FileData) { Log.Debug("fileData = {filedata}", fileData); string[] splitData = fileData.Split(' ', StringSplitOptions.RemoveEmptyEntries); Log.Debug("splitData = {@splitData}", splitData); string checksum = splitData[0]; int size = int.Parse(splitData[1]); string filename = splitData[2]; releasePackageFiles.Add(new PackageFile(filename, checksum, size)); } // remove any empty files (and the .gz version of them) // NOTE: I do this since empty gz files have a size // WARN: The `.ToList` is essential to ensure we COPY the list and not use references to the original items var emptyFiles = releasePackageFiles.Where(package => package.size == 0).ToList(); foreach (var file in emptyFiles) { releasePackageFiles.RemoveAll(package => package.filename == file.filename || package.filename == $"{file.filename}.gz" ); } PackageFiles = releasePackageFiles; // After removing the empty files we need to make sure that we remove the architectures that don't have any files foreach (var arch in Architectures.ToList()) // WARN: `.ToList` is used again to copy the list so we can iterate and operate { if (PackageFiles.Where(file => file.filename.Contains(arch)).Count() == 0) { Architectures.Remove(arch); } } } private static FormatException _lineFormatException(string field) { return new FormatException($"Release does not contain a valid {field} line"); } } public record PackageFile(string filename, string Checksum, int size);