From 60e06b21444e18d83992a862576f695fa5789b3d Mon Sep 17 00:00:00 2001 From: Robert Morrison Date: Thu, 8 Jun 2023 04:36:31 +0100 Subject: [PATCH] Another bad commit Lots of changes here... - Removed code using RecursivExtractor due to bad usage of /tmp Note: the code that used RecursiveExtractor may not have been in the previous commit - Created _functioning_ implementation of DebUnpacker - Restructured Project layout. TODO: - Possibly rewrite how the DebUnpacker works to have the file contents part of an entry. - Further restructuring and refactoring to make the code a little neater. - Add code for the final unpack steps Un-XZ -> untar -> write to disk - Add code to install - Add code for modified post-install pre-remove etc.. This is kinda needed to ensure proper system integration of new packages. --- DebArchive.cs | 60 --- DebUnpack.cs | 145 +++++++ Installer/Installer.cs | 20 + Package/Package.JsonConstructor.cs | 16 + Package.cs => Package/Package.cs | 16 +- Program.Downloaders.cs | 126 ++++++ Program.HelperFunctions.cs | 67 +++ Program.Renderers.cs | 150 +++++++ Program.cs | 405 ++++++------------- ReleaseData/ReleaseData.JsonConstructor.cs | 13 + ReleaseData.cs => ReleaseData/ReleaseData.cs | 76 ++-- Version.cs | 127 ------ Version/Version.IComparable.cs | 75 ++++ Version/Version.IEquatable.cs | 22 + Version/Version.cs | 41 ++ edge-install.csproj | 1 + 16 files changed, 827 insertions(+), 533 deletions(-) delete mode 100644 DebArchive.cs create mode 100644 DebUnpack.cs create mode 100644 Installer/Installer.cs create mode 100644 Package/Package.JsonConstructor.cs rename Package.cs => Package/Package.cs (75%) create mode 100644 Program.Downloaders.cs create mode 100644 Program.HelperFunctions.cs create mode 100644 Program.Renderers.cs create mode 100644 ReleaseData/ReleaseData.JsonConstructor.cs rename ReleaseData.cs => ReleaseData/ReleaseData.cs (66%) delete mode 100644 Version.cs create mode 100644 Version/Version.IComparable.cs create mode 100644 Version/Version.IEquatable.cs create mode 100644 Version/Version.cs diff --git a/DebArchive.cs b/DebArchive.cs deleted file mode 100644 index b708007..0000000 --- a/DebArchive.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Experimental code to open and dissect a `.deb` archive - -static class DebArchive -{ - const string DPKG_AR_MAGIC = "!\n"; - const string DPKG_AR_FMAG = "`\n"; - - - private static dpkg_ar dpkg_ar_fdopen(string filename, FileInfo fi) - { - return new dpkg_ar( - filename, - fi.UnixFileMode, - fi.Length, - fi.LastWriteTime, - fi); - } - - private static dpkg_ar dpkg_ar_open(string filename) - { - FileInfo fi = new FileInfo(filename); - - return dpkg_ar_fdopen(filename, fi); - } -} - -sealed class dpkg_ar -{ - string name; - UnixFileMode mode; - long size; - DateTime time; - FileInfo fi; - - public dpkg_ar(string name, - UnixFileMode mode, - long size, - DateTime time, - FileInfo fi) - { - this.name = name; - this.mode = mode; - this.size = size; - this.time = time; - this.fi = fi; - } -} - -/* TODO: - - Read the sourcecode for dpkg and find the c# equivalents - to extract an archive. - - If neccessary write a parser manually. -*/ -/* NOTE: - - dpkg_ar is just FileInfo - - We might need to define dpkg_ar_hdr - - Pretty sure we can just find line ending in "`\n" - and parse that though. Split on (" ") - - Should then be able to read the files by streaming `size` bytes from the position after the and marker -*/ diff --git a/DebUnpack.cs b/DebUnpack.cs new file mode 100644 index 0000000..ee03d6b --- /dev/null +++ b/DebUnpack.cs @@ -0,0 +1,145 @@ +using System.Text; +using Serilog; +namespace EdgeInstaller; + +sealed class DebFile : IDisposable +{ + const int HeaderBytes = 60; + const string FileMagic = "!\n"; + + private MemoryStream _fileStream; + + public List fileEntries { get; private set; } + + public DebFile(Stream fs) + { + Log.Information("Creating new debFile from stream"); + fileEntries = new(); + if (fs is null) + { + throw new NullReferenceException(); + } + + fs.Position = 0; + + // read the first 8 bytes and check for the signature + byte[] magicBuffer = new byte[8]; + + fs.Read(magicBuffer); + Log.Debug("Magic = {magicBuffer}", Encoding.ASCII.GetString(magicBuffer)); + if (Encoding.ASCII.GetString(magicBuffer) != FileMagic) + { + throw new ArgumentException(message: "Magic Fail"); + } + + fs.Position = 0; + _fileStream = new(); + fs.CopyTo(_fileStream); + + fs.Dispose(); + // if we got here then we must have a proper archive. + // We shall now get the files that are inside. + + // from OUR copy of the stream we seek to after the magic byte + _fileStream.Position = 8; + while (true) // beware making an infinite loop + { + // Read a header + byte[] header = new byte[60]; + var read = _fileStream.Read(header); + // If nothing was read we have hit the end + if (read < 60) // also handle broken headers + { + break; + } + // Otherwise process as a header + var entry = new DebEntry(header, _fileStream.Position); + fileEntries.Add(entry); + + // Using the obtained size seek forwards to the next potential header + _fileStream.Seek(entry.sizeBytes, SeekOrigin.Current); + } + } + + public bool getFile(DebEntry file, out Stream output) + { + // If we are given a file that isn't in this archive we exit nicely + if (!fileEntries.Contains(file)) + { + Log.Error("The given file entry is not valid for this DebFile"); + output = Stream.Null; + return false; + } + + // Since we have a file that is in the archive + // grab the metadata we need + long offset = file.offset; + long size = file.sizeBytes; + // stream the file to the output stream + _fileStream.Seek(offset, SeekOrigin.Begin); + byte[] fileBuffer = new byte[size]; + + var count = _fileStream.Read(fileBuffer); + if (count != size) + { + Log.Error("Didn't read the correct amount of data from stream (expected:{size} got:{count})", size, count); + output = Stream.Null; + return false; + } + + output = new MemoryStream(fileBuffer); // convert the read buffer to a stream + return true; + } + + public void Dispose() + { // make sure we can clean up our own mess + _fileStream.Dispose(); + } +} + +sealed class DebEntry +{ + // Escape sequnces are used in this string read as "`\n" + const string HeaderMagic = "\x60\n"; + + public string name { get; private set; } + public long mtime { get; private set; } + public int mode { get; private set; } + public int gid { get; private set; } + public int uid { get; private set; } + public long sizeBytes { get; private set; } + public long offset { get; private set; } // where does the file start + + public DebEntry(byte[] buffer, long Position) + { + Log.Debug("Reading header at position {p}", Position - 60); // since we are being passed a header we take 60 to get the header position + if (buffer is null) + { + throw new NullReferenceException(); + } + + if (buffer.Length < 60) + { + throw new ArgumentException("buffer too short"); + } + + name = Encoding.ASCII.GetString(buffer[0..16]); + mtime = long.Parse(Encoding.ASCII.GetString(buffer[16..28])); + mode = int.Parse(Encoding.ASCII.GetString(buffer[28..34])); + gid = int.Parse(Encoding.ASCII.GetString(buffer[34..40])); + uid = int.Parse(Encoding.ASCII.GetString(buffer[40..48])); + sizeBytes = long.Parse(Encoding.ASCII.GetString(buffer[48..58])); + string magic = Encoding.ASCII.GetString(buffer[58..^0]); + + offset = Position; + + Log.Debug("Header Magic = {magic}", magic); + Log.Debug("{@this}", this); + if (magic != HeaderMagic) + { + throw new ArgumentException("magic failed"); + } + + } + +} diff --git a/Installer/Installer.cs b/Installer/Installer.cs new file mode 100644 index 0000000..8095723 --- /dev/null +++ b/Installer/Installer.cs @@ -0,0 +1,20 @@ +namespace EdgeInstaller; + +// This class is what will do the heavy lifting. +// the UI will all be handled seperately +public class Installer +{ + + private static readonly string DataDir = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ClosedLess", + "EdgeInstall-Jankmode"); + + + public Installer(HttpClient client) + { + } + + +} diff --git a/Package/Package.JsonConstructor.cs b/Package/Package.JsonConstructor.cs new file mode 100644 index 0000000..736fdc3 --- /dev/null +++ b/Package/Package.JsonConstructor.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace EdgeInstaller; + +public sealed partial class Package +{ + [JsonConstructor] // NOTE: without this it would be impossible to deserialise a Package + private Package(string PackageName, Version Version, int Size, string SHA256, string Filename) + { + this.PackageName = PackageName; + this.Version = Version; + this.Size = Size; + this.SHA256 = SHA256; + this.Filename = Filename; + } +} diff --git a/Package.cs b/Package/Package.cs similarity index 75% rename from Package.cs rename to Package/Package.cs index 57b01fc..f4f1bda 100644 --- a/Package.cs +++ b/Package/Package.cs @@ -1,9 +1,9 @@ using Serilog; -using System.Text.Json.Serialization; // Defines a Package +namespace EdgeInstaller; -sealed class Package +sealed partial class Package { public string PackageName { get; private set; } @@ -12,16 +12,6 @@ sealed class Package public string SHA256 { get; private set; } public string Filename { get; private set; } - [JsonConstructor] // NOTE: without this it would be impossible to deserialise a Package - private Package(string PackageName, Version Version, int Size, string SHA256, string Filename) - { - this.PackageName = PackageName; - this.Version = Version; - this.Size = Size; - this.SHA256 = SHA256; - this.Filename = Filename; - } - public Package(string packageString) { Log.Information("Creating package from string"); @@ -61,6 +51,4 @@ sealed class Package /* TODO: capture dependencies and create method to find them. - This would probably need distro specific lookup tables. - - Also would ideally need code to shell out (at root level) - to perform installs... */ diff --git a/Program.Downloaders.cs b/Program.Downloaders.cs new file mode 100644 index 0000000..c5a056e --- /dev/null +++ b/Program.Downloaders.cs @@ -0,0 +1,126 @@ +using Spectre.Console; +using Serilog; + +namespace EdgeInstaller; + +public static partial class EdgeInstall +{ + + + private static async Task GetRelease() + { + string releaseURL = "dists/stable/Release"; + using HttpResponseMessage response = await _client.GetAsync(releaseURL); + await Task.Delay(5000); + Log.Information("Getting release from {url}", response.Headers.Location); + response.EnsureSuccessStatusCode(); //WARN: this can throw an exception, Please wrap in try/catch or rewrite function + + var body = await response.Content.ReadAsStringAsync(); + Log.Debug("{body}", body); + return body; + } + + static async Task downloadFile(string URL, string DestinationDirectory) + { + + Uri file = new Uri(URL); + + string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last()); + + using HttpResponseMessage response = await _client.GetAsync(URL); + response.EnsureSuccessStatusCode(); + + await Task.Delay(2000); + + var fileData = response.Content.ReadAsStreamAsync(); + + using var fileStream = new FileStream(fileLocation, FileMode.OpenOrCreate); + await response.Content.CopyToAsync(fileStream); + + return fileLocation; + } + + + // This overload of downloadFile allows you to get the progess + static async Task downloadFile(string URL, string DestinationDirectory, ProgressTask task) + { + Uri file = new Uri(URL, UriKind.RelativeOrAbsolute); // allow this to work regardless of relativity + + if (!file.IsAbsoluteUri && _client.BaseAddress is not null) + { // Ego te absolvo + file = new Uri(_client.BaseAddress, URL); + } + + string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last()); + + using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead)) + { + response.EnsureSuccessStatusCode(); + + + task.MaxValue = response.Content.Headers.ContentLength ?? 0; + task.StartTask(); + await Task.Delay(2000); + + AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] ({task.MaxValue} Bytes)"); + + using (var contentStream = await response.Content.ReadAsStreamAsync()) + using (var fileStream = File.Open(fileLocation, FileMode.OpenOrCreate)) + { + var buffer = new byte[downloadBufferBYTES]; + while (true) + { + var read = await contentStream.ReadAsync(buffer, 0, buffer.Length); + await Task.Delay(downloadDelayMS); + if (read == 0) + { + AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]"); + break; + } + task.Increment(read); + + await fileStream.WriteAsync(buffer, 0, read); + } + } + } + + return fileLocation; + } + + // This overload of downloadFile allows you to get the progess + static async Task downloadStream(string URL, ProgressTask task) + { + MemoryStream outputStream = new(); + using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead)) + { + response.EnsureSuccessStatusCode(); + + + task.MaxValue = response.Content.Headers.ContentLength ?? 0; + task.StartTask(); + await Task.Delay(2000); + + AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] to memory ({task.MaxValue} Bytes)"); + + int bufferSize = (int)Math.Round(task.MaxValue / 100); + using (var contentStream = await response.Content.ReadAsStreamAsync()) + { + var buffer = new byte[bufferSize]; + while (true) + { + var read = await contentStream.ReadAsync(buffer, 0, buffer.Length); + await Task.Delay(downloadDelayMS); + if (read == 0) + { + AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]"); + break; + } + task.Increment(read); + + await outputStream.WriteAsync(buffer, 0, read); + } + } + } + return outputStream; + } +} diff --git a/Program.HelperFunctions.cs b/Program.HelperFunctions.cs new file mode 100644 index 0000000..c9d9f8c --- /dev/null +++ b/Program.HelperFunctions.cs @@ -0,0 +1,67 @@ +using Serilog; +using Spectre.Console; +using System.IO.Compression; +using System.Text.RegularExpressions; +using System.Security.Cryptography; +namespace EdgeInstaller; + +public static partial class EdgeInstall +{ + private static string GetPackagesString(string filepath) + { + using (var CompressedStream = File.Open(filepath, FileMode.Open)) + using (var decompressor = new GZipStream(CompressedStream, CompressionMode.Decompress)) + using (var streamReader = new StreamReader(decompressor)) + { + return streamReader.ReadToEnd(); + } + } + + /* + * HACK: Spectre.Console uses fixed regex to detect terminal capabilites + * This function tests for known working terminals that are currently not + * detected by AnsiDetector + */ + private static void BUGFIX_ansiDetection() + { + Regex[] regexes = { + new("foot") + }; + AnsiConsole.Profile.Capabilities.Ansi = true; + AnsiConsole.Profile.Capabilities.Legacy = false; + } + + static bool ValidateChecksum(FileInfo file, string checksum) + { + using (FileStream fs = file.Open(FileMode.Open)) + { + return ValidateChecksum(fs, checksum); + } + } + + static bool ValidateChecksum(Stream stream, string checksum) + { + stream.Position = 0; + using (SHA256 mySHA256 = SHA256.Create()) + { + byte[] hashValue = mySHA256.ComputeHash(stream); + if (hashValue is null) + { + throw new NullReferenceException("Hash be null??"); + } + Log.Debug("Computed hash = {hash}", hashValue); + byte[] storedHash = Convert.FromHexString(checksum); + Log.Debug("Stored Hash = {hash}", storedHash); + + Log.Debug("Compare result = {result}", hashValue.SequenceEqual(storedHash)); + return (storedHash.SequenceEqual(hashValue)); + } + } + + + static Version GetCurrentVersion() + { + + return new("0.0.0.0-0"); + } +} diff --git a/Program.Renderers.cs b/Program.Renderers.cs new file mode 100644 index 0000000..77dba42 --- /dev/null +++ b/Program.Renderers.cs @@ -0,0 +1,150 @@ +using Spectre.Console; + +namespace EdgeInstaller; + +public static partial class EdgeInstall +{ + + static void RenderPackageAsTable(Package package) + { + Table Package = new Table() + .Centered() + .Border(TableBorder.Rounded) + .HideHeaders() + .Title("Package To Install"); + + AnsiConsole.Live(Package) + .AutoClear(false) + .Start(context => + { + Package.AddColumns("", ""); + context.Refresh(); + Thread.Sleep(250); + + Package.AddRow("[bold yellow]Package Name : [/]", $"{package.PackageName}"); + context.Refresh(); + Thread.Sleep(100); + Package.AddRow("[bold yellow]Version : [/]", $"{package.Version.ToString()}"); + context.Refresh(); + Thread.Sleep(100); + Package.AddRow("[bold yellow]Size : [/]", $"{package.Size.ToString()}B"); + context.Refresh(); + Thread.Sleep(100); + Package.AddRow("[bold yellow]SHA256 : [/]", $"{package.SHA256}"); + context.Refresh(); + Thread.Sleep(100); + Package.AddRow("[bold yellow]Filename : [/]", $"{package.Filename}"); + context.Refresh(); + }); + } + + static void RenderPackagesTable(List Packages) + { + Table PackageTable = new Table() + .Centered() + .Expand() + .Border(TableBorder.Rounded) + .Title("Packages"); + + AnsiConsole.Live(PackageTable) + .AutoClear(false) + .Start(context => + { + PackageTable + .AddColumn("[bold]PackageName[/]") + .AddColumn("[bold]Version[/]") + .AddColumn("[bold]Size (bytes)[/]") + .AddColumn("[bold]SHA256[/]") + .AddColumn("[bold]Filename[/]"); + context.Refresh(); + Thread.Sleep(500); + + foreach (var pack in Packages) + { + PackageTable.AddRow( + FormatPackageName(pack.PackageName), + $"[i blue]{pack.Version.ToString()}[/]", + $"[grey]{pack.Size.ToString()}[/][b]B[/]", + pack.SHA256, + pack.Filename); + context.Refresh(); + Thread.Sleep(100); + } + }); + + } + + static string FormatPackageName(string name) + { + if (name.Contains("dev")) + { + return $"[red]{name}[/]"; + } + if (name.Contains("beta")) + { + return $"[yellow]{name}[/]"; + } + if (name.Contains("stable")) + { + return $"[green]{name}[/]"; + } + return name; + } + + + static void RenderReleaseTable(ReleaseData data) + { + // Lets output this ReleaseData now + Table ReleaseTable = new Table() + .Centered() + .Border(TableBorder.Rounded) + .Expand() + .Title("Release Data"); + + AnsiConsole.Live(ReleaseTable) + .AutoClear(false) + .Start(context => + { + ReleaseTable.AddColumn("[bold]Field[/]"); + ReleaseTable.AddColumn("[bold]Value[/]"); + context.Refresh(); + Thread.Sleep(500); + + ReleaseTable.AddRow("[bold yellow]Date:[/]", $"[blue]{data.Date:R}[/]"); + context.Refresh(); + Thread.Sleep(500); + + Table archs = new Table() + .Centered() + .Border(TableBorder.None) + .HideFooters() + .HideHeaders() + .AddColumn(new TableColumn(Text.Empty)); + ReleaseTable.AddRow(new Markup("[bold yellow]Architectures:[/]"), archs); + foreach (var arch in data.Architectures) + { + archs.AddRow(arch); + context.Refresh(); + Thread.Sleep(250); + } + + Table PackageFilesTable = new Table() + .Centered() + .Border(TableBorder.Square) + .BorderColor(Color.Red) + .AddColumn("[bold]Filename[/]") + .AddColumn("[bold]Checksum[/]") + .AddColumn("[bold]Size[/]"); + ReleaseTable.AddRow( + new Markup("[bold yellow]Package Files:[/]"), + PackageFilesTable); + context.Refresh(); + foreach (var PackageFile in data.PackageFiles) + { + PackageFilesTable.AddRow(PackageFile.filename, PackageFile.Checksum, PackageFile.size.ToString()); + context.Refresh(); + Thread.Sleep(250); + } + }); + } +} diff --git a/Program.cs b/Program.cs index 7945d62..965a547 100644 --- a/Program.cs +++ b/Program.cs @@ -1,15 +1,22 @@ //using System.CommandLine; using Serilog; using Spectre.Console; -using System.IO.Compression; using System.Text.Json; -using System.Security.Cryptography; using System.Text; namespace EdgeInstaller; -public static class EdgeInstall +public static partial class EdgeInstall { + // NOTE: tweaking these values can be used to make downloads look more "fancy" + // A.K.A make downloads take longer so the progress bar appears for a little longer + private static readonly int downloadDelayMS = 0; + private static readonly int downloadBufferBYTES = 10240; + + // The default architecture to get + // likely will go unused until proper cli interface is added + private static readonly string _DefaultArch = "amd64"; + private static HttpClient _client = new HttpClient(); private static readonly string DataDir = @@ -18,23 +25,19 @@ public static class EdgeInstall "ClosedLess", "EdgeInstall-Jankmode"); + public static async Task Main() { - // WARN: Hardcoded variables are bad m'kay... - string Arch = "amd64"; - + _client.BaseAddress = new Uri("https://packages.microsoft.com/repos/edge/"); // Ensure DataDir exists. // NOTE: Directory.CreateDirectory will ALWAYS create the entire path Directory.CreateDirectory(DataDir); - // TODO: wrap this code in foot detection since foot isn't detected properly - // BUG: Report detection error to Spectre.Console (foot == xterm) - // HACK: We have to force this right now for foot to work properly... - AnsiConsole.Console.Profile.Capabilities.Ansi = true; + BUGFIX_ansiDetection(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() - .WriteTo.File(".EdgeInstaller.Log") // Only ever log to the logfile + .WriteTo.File(Path.Combine(DataDir, ".EdgeInstaller.Log")) // Only ever log to the logfile .CreateLogger(); var Release = AnsiConsole.Status() @@ -49,26 +52,26 @@ public static class EdgeInstall Log.Debug("{rs}", ReleaseString); AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]"); - ReleaseData data = getReleaseData(ReleaseString); + ReleaseData data = new(ReleaseString); RenderReleaseTable(data); - Arch = AnsiConsole.Prompt( - new SelectionPrompt() - .Title("Choose Architecture") - .PageSize(10) - .MoreChoicesText("[grey]Scroll for more...[/]") - .AddChoices(data.Architectures) - ); + // only select arch if needed + string TargetArch = data.Architectures.Count > 1 ? AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Choose Architecture") + .PageSize(5) + .MoreChoicesText("[grey]Scroll for more...[/]") + .AddChoices(data.Architectures) + ) : data.Architectures.First(); // create a directory for the chosen architecture // NOTE: we also grab the path here so we can use it later. - var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, Arch)).FullName; + var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, TargetArch)).FullName; // Check if the Release data we just grabbed is newer than the one that exists. // NOTE: we only do this if our chosen architecture hasn't been downloaded - // OOF semaphores.. bool NeedDownload = true; Log.Information("Reached Package download Stage"); @@ -98,34 +101,34 @@ public static class EdgeInstall .Where( file => file.filename.Contains(".gz") && - file.filename.Contains(Arch)) // ensure we only fetch for the chosen architecture + file.filename.Contains(TargetArch)) // ensure we only fetch for the chosen architecture .First(); if (gzFile is null) { throw new NullReferenceException("OOPS gzfile null"); } - string URL = "http://packages.microsoft.com/repos/edge/dists/stable/" + gzFile.filename; + string URL = "dists/stable/" + gzFile.filename; string filename = await AnsiConsole.Progress() .Columns(new ProgressColumn[] { - new TaskDescriptionColumn(), - new ProgressBarColumn(), - new PercentageColumn(), - new RemainingTimeColumn(), - new SpinnerColumn(), + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new RemainingTimeColumn(), + new SpinnerColumn(), }) - .StartAsync(async ctx => - { - var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false }); + .StartAsync(async ctx => + { + var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false }); - return await downloadFile(URL, Archdir, task); + return await downloadFile(URL, Archdir, task); - }); + }); string JSONdata = JsonSerializer.Serialize(data); File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata); - if (!ValidateChecksum(filename, gzFile.Checksum)) + if (!ValidateChecksum(new FileInfo(filename), gzFile.Checksum)) { File.Delete(filename); Environment.Exit(1); @@ -145,6 +148,10 @@ public static class EdgeInstall if (line == "") { string PackageString = sb.ToString(); + if (PackageString == "") + { + continue; // The last "package" will be empty + } sb.Clear(); // ensure we clear the StringBuilder when we are done with it. try @@ -171,15 +178,15 @@ public static class EdgeInstall .ThenByDescending(package => package.Version).ToList(); // Print any packages we have var Variant = AnsiConsole.Prompt( - new SelectionPrompt() - .Title("Choose variant") - .PageSize(4) - .AddChoices(new[] { + new SelectionPrompt() + .Title("Choose variant") + .PageSize(4) + .AddChoices(new[] { "stable", "beta", "dev", "all" - }) + }) ); @@ -191,19 +198,19 @@ public static class EdgeInstall else { VariantPackages = packages - .Where(package => + .Where(package => package.PackageName.Contains(Variant)) - .ToList(); + .ToList(); } RenderPackagesTable(VariantPackages); var Package = AnsiConsole.Prompt( - new SelectionPrompt() - .Title("Choose Package to Install") - .PageSize(5) - .AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct()) + new SelectionPrompt() + .Title("Choose Package to Install") + .PageSize(5) + .AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct()) ); var ChosenPackages = packages @@ -215,261 +222,77 @@ public static class EdgeInstall RenderPackageAsTable(LatestVersion); - return 0; - } + var versionPath = LatestVersion.Filename + .Replace("pool/main/m/", "") + .Replace(".deb", ""); - static void RenderPackageAsTable(Package package) - { - Table Package = new Table() - .Centered() - .Border(TableBorder.Rounded) - .HideHeaders() - .Title("Package To Install"); + var versionsDir = Path.Join(Archdir, "Versions"); + // Let's get and unpack the latest version - AnsiConsole.Live(Package) - .AutoClear(false) - .Start(context => - { - Package.AddColumns("", ""); - context.Refresh(); - Thread.Sleep(250); - - Package.AddRow("[bold yellow]Package Name : [/]", $"{package.PackageName}"); - context.Refresh(); - Thread.Sleep(100); - Package.AddRow("[bold yellow]Version : [/]", $"{package.Version.ToString()}"); - context.Refresh(); - Thread.Sleep(100); - Package.AddRow("[bold yellow]Size : [/]", $"{package.Size.ToString()}B"); - context.Refresh(); - Thread.Sleep(100); - Package.AddRow("[bold yellow]SHA256 : [/]", $"{package.SHA256}"); - context.Refresh(); - Thread.Sleep(100); - Package.AddRow("[bold yellow]Filename : [/]", $"{package.Filename}"); - context.Refresh(); - }); - } - - static void RenderPackagesTable(List Packages) - { - Table PackageTable = new Table() - .Centered() - .Expand() - .Border(TableBorder.Rounded) - .Title("Packages"); - - AnsiConsole.Live(PackageTable) - .AutoClear(false) - .Start(context => - { - PackageTable - .AddColumn("[bold]PackageName[/]") - .AddColumn("[bold]Version[/]") - .AddColumn("[bold]Size (bytes)[/]") - .AddColumn("[bold]SHA256[/]") - .AddColumn("[bold]Filename[/]"); - context.Refresh(); - Thread.Sleep(500); - - foreach (var pack in Packages) - { - PackageTable.AddRow(FormatPackageName(pack.PackageName), $"[i blue]{pack.Version.ToString()}[/]", $"[grey]{pack.Size.ToString()}[/][b]B[/]", pack.SHA256, pack.Filename); - context.Refresh(); - Thread.Sleep(100); - } - }); - - } - - static string FormatPackageName(string name) - { - if (name.Contains("dev")) + if (true)//!Directory.Exists(Path.Join(versionsDir, versionPath))) { - return $"[red]{name}[/]"; - } - if (name.Contains("beta")) - { - return $"[yellow]{name}[/]"; - } - if (name.Contains("stable")) - { - return $"[green]{name}[/]"; - } - return name; - } - - static async Task GetRelease() - { - string releaseURL = "http://packages.microsoft.com/repos/edge/dists/stable/Release"; - using HttpResponseMessage response = await _client.GetAsync(releaseURL); - await Task.Delay(5000); - response.EnsureSuccessStatusCode(); // WARNING: this can throw an exception, Please wrap in try/catch or rewrite function - - var body = await response.Content.ReadAsStringAsync(); - Log.Debug("{body}", body); - return body; - } - - static ReleaseData getReleaseData(string release) - { - return new ReleaseData(release); - } - - static async Task downloadFile(string URL, string DestinationDirectory) - { - - Uri file = new Uri(URL); - - string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last()); - - using HttpResponseMessage response = await _client.GetAsync(URL); - response.EnsureSuccessStatusCode(); - - await Task.Delay(2000); - - var fileData = response.Content.ReadAsStreamAsync(); - - using var fileStream = new FileStream(fileLocation, FileMode.OpenOrCreate); - await response.Content.CopyToAsync(fileStream); - - return fileLocation; - } - - static async Task downloadFile(string URL, string DestinationDirectory, ProgressTask task) - { - Uri file = new Uri(URL); - string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last()); - - using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead)) - { - response.EnsureSuccessStatusCode(); - - - task.MaxValue = response.Content.Headers.ContentLength ?? 0; - task.StartTask(); - await Task.Delay(2000); - - AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] ({task.MaxValue} Bytes)"); - - using (var contentStream = await response.Content.ReadAsStreamAsync()) - using (var fileStream = File.Open(fileLocation, FileMode.OpenOrCreate)) - { - var buffer = new byte[512]; - while (true) - { - var read = await contentStream.ReadAsync(buffer, 0, buffer.Length); - await Task.Delay(500); - if (read == 0) + using (MemoryStream debStream = + await AnsiConsole.Progress() + .Columns(new ProgressColumn[] + { + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new RemainingTimeColumn(), + new SpinnerColumn(), + }) + .StartAsync(async ctx => { - AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]"); - break; + var task = ctx.AddTask(LatestVersion.Filename, new ProgressTaskSettings { AutoStart = false }); + + return await downloadStream(LatestVersion.Filename, task); + })) + { + + if (!ValidateChecksum(debStream, LatestVersion.SHA256)) + { + AnsiConsole.MarkupLine("[red b]ERROR:[/] checksum did not validate"); + Environment.Exit(1); + } + + using (DebFile debfile = new DebFile(debStream)) + { + foreach (var fileEntry in debfile.fileEntries) + { + AnsiConsole.MarkupLineInterpolated($"FileName: {fileEntry.name}"); + AnsiConsole.MarkupLineInterpolated($"FilePos: {fileEntry.offset}"); + AnsiConsole.MarkupLineInterpolated($"FileSize: {fileEntry.sizeBytes}"); + + // NOTE: This is dumb test code + // to verify the files are actually being "extracted" properly + + if (!debfile.getFile(fileEntry, out Stream contents)) + { + return 1; + } + + // Now we write this to tmp. + // WARN: Without deleting these you will fill up your ram + + var temp = Path.GetTempFileName(); + + using (var tmpfile = File.Open(temp, FileMode.Open)) + { + contents.CopyTo(tmpfile); + contents.Dispose(); + } } - task.Increment(read); - - await fileStream.WriteAsync(buffer, 0, read); } } - } - return fileLocation; - } - - static bool ValidateChecksum(string path, string checksum) - { - using (SHA256 mySHA256 = SHA256.Create()) - { - using (FileStream fs = File.Open(path, FileMode.Open)) - { - fs.Position = 0; - - byte[] hashValue = mySHA256.ComputeHash(fs); - if (hashValue is null) - { - throw new NullReferenceException("Hash be null??"); - } - Log.Debug("Computed hash = {hash}", hashValue); - byte[] storedHash = Convert.FromHexString(checksum); - Log.Debug("Stored Hash = {hash}", storedHash); - - Log.Debug("Compare result = {result}", hashValue.SequenceEqual(storedHash)); - return (storedHash.SequenceEqual(hashValue)); - } + return 0; } } - - static void RenderReleaseTable(ReleaseData data) - { - // Lets output this ReleaseData now - Table ReleaseTable = new Table() - .Centered() - .Border(TableBorder.Rounded) - .Expand() - .Title("Release Data"); - - AnsiConsole.Live(ReleaseTable) - .AutoClear(false) - .Start(context => - { - ReleaseTable.AddColumn("[bold]Field[/]"); - ReleaseTable.AddColumn("[bold]Value[/]"); - context.Refresh(); - Thread.Sleep(500); - - ReleaseTable.AddRow("[bold yellow]Date:[/]", $"[blue]{data.Date:R}[/]"); - context.Refresh(); - Thread.Sleep(500); - - Table archs = new Table() - .Centered() - .Border(TableBorder.None) - .HideFooters() - .HideHeaders() - .AddColumn(new TableColumn(Text.Empty)); - ReleaseTable.AddRow(new Markup("[bold yellow]Architectures:[/]"), archs); - foreach (var arch in data.Architectures) - { - archs.AddRow(arch); - context.Refresh(); - Thread.Sleep(250); - } - - Table PackageFilesTable = new Table() - .Centered() - .Border(TableBorder.Square) - .BorderColor(Color.Red) - .AddColumn("[bold]Filename[/]") - .AddColumn("[bold]Checksum[/]") - .AddColumn("[bold]Size[/]"); - ReleaseTable.AddRow( - new Markup("[bold yellow]Package Files:[/]"), - PackageFilesTable); - context.Refresh(); - foreach (var PackageFile in data.PackageFiles) - { - PackageFilesTable.AddRow(PackageFile.filename, PackageFile.Checksum, PackageFile.size.ToString()); - context.Refresh(); - Thread.Sleep(250); - } - }); - } - - private static string GetPackagesString(string filepath) - { - using (var CompressedStream = File.Open(filepath, FileMode.Open)) - using (var decompressor = new GZipStream(CompressedStream, CompressionMode.Decompress)) - using (var streamReader = new StreamReader(decompressor)) - { - return streamReader.ReadToEnd(); - } - } - /*TODO: - - Parse package to find latest version - - Check version - - Offer update - - Make SENSIBLE backup - - Do install - = if install fails revert backup - */ + - Check version + - Offer update + - Make SENSIBLE backup + - Do install + = if install fails revert backup + */ } diff --git a/ReleaseData/ReleaseData.JsonConstructor.cs b/ReleaseData/ReleaseData.JsonConstructor.cs new file mode 100644 index 0000000..f2c40e4 --- /dev/null +++ b/ReleaseData/ReleaseData.JsonConstructor.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +namespace EdgeInstaller; + +public sealed partial class ReleaseData +{ + [JsonConstructor] + public ReleaseData(DateTime date, List architectures, List packageFiles) + { + this.Date = date; + this.Architectures = architectures; + this.PackageFiles = packageFiles; + } +} diff --git a/ReleaseData.cs b/ReleaseData/ReleaseData.cs similarity index 66% rename from ReleaseData.cs rename to ReleaseData/ReleaseData.cs index 5d9f498..0dbd002 100644 --- a/ReleaseData.cs +++ b/ReleaseData/ReleaseData.cs @@ -2,22 +2,15 @@ // 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 sealed partial 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) { @@ -30,31 +23,19 @@ sealed class 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( + Date = DateTime.Parse( + 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"); - } + .First() + .Split(": ") + .Last() + ); // Safely find and add the possible architectures string ArchitecturesLine = @@ -67,19 +48,25 @@ sealed class ReleaseData 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 + .Split(": ").Last() // We only want items after the label + .Split(" ", StringSplitOptions.RemoveEmptyEntries) + .ToList(); 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 + + /* + * Package files are grabbed here. It is worth noting that empty files + * are removed (along with the .gz versions) this avoids displaying impossible + * options to the user + */ + // 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(); + // NOTE: This is probably the cleanest way to do this. + List SHA256FileData = ReleaseLines.Where((_, 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 @@ -96,16 +83,19 @@ sealed class ReleaseData } // remove any empty files (and the .gz version of them) - // NOTE: I do this since empty gz files have a size + // NOTE: Empty gz files still have a size due to headers // 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(); + var emptyFiles = releasePackageFiles + .Where(package => package.size == 0) + .ToList(); foreach (var file in emptyFiles) { - releasePackageFiles.RemoveAll(package => + releasePackageFiles + .RemoveAll(package => package.filename == file.filename || package.filename == $"{file.filename}.gz" - ); + ); } PackageFiles = releasePackageFiles; @@ -113,7 +103,11 @@ sealed class ReleaseData // 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 + /* + * WARN: `.ToList` is used again to copy the list so we can iterate and operate + * on the list at the same time. This could be unsafe, but in this scenario + * should never cause any issues. + */ { if (PackageFiles.Where(file => file.filename.Contains(arch)).Count() == 0) { diff --git a/Version.cs b/Version.cs deleted file mode 100644 index 231a718..0000000 --- a/Version.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Serilog; -// Make a version since it is so much easier to compare when the class handles it. - -sealed class Version : IComparable, IEquatable -{ - public int Major { get; private set; } - public int Minor { get; private set; } - public int Build { get; private set; } - public int Patch { get; private set; } - public int Subpatch { get; private set; } - - - public Version(string versionString) - { - Log.Information("Creating version from string {versionString}", versionString); - if (versionString == "") - { - throw new ArgumentException("The passed version string was empty", "versionString"); - } - - var splitVersion = versionString.Split('.'); - Log.Debug("SplitVersion = {@splitversion}", splitVersion); - Major = int.Parse(splitVersion[0]); - Minor = int.Parse(splitVersion[1]); - Build = int.Parse(splitVersion[2]); - - var splitPatch = splitVersion[3].Split('-'); - Log.Debug("SplitPatch = {@splitPatch}", splitPatch); - Log.Debug("SplitPatch values : [0] = {0}, [1] = {1}", splitPatch[0], splitPatch[1]); - Patch = int.Parse(splitPatch[0]); - Subpatch = int.Parse(splitPatch[1]); - Log.Debug("Version Created: {@version}", this); - } - - public override string ToString() - { - return $"{Major}.{Minor}.{Build}.{Patch}-{Subpatch}"; - } - - public int CompareTo(Version? other) - { - if (other is null) - { - return 1; - } - - // full Equality is easy since everything should be the same. - // OPTIM: Doing this here is techinally more effecient than baking it in to the later code - // Bools and shit - if (this.Major == other.Major && - this.Minor == other.Minor && - this.Build == other.Build && - this.Patch == other.Patch && - this.Subpatch == other.Subpatch) - { - return 0; - } - - // Compare major version - if (Major != other.Major) - return Major.CompareTo(other.Major); - - // Compare minor version - if (Minor != other.Minor) - return Minor.CompareTo(other.Minor); - - // Compare build number - if (Build != other.Build) - return Build.CompareTo(other.Build); - - // Compare patch number - if (Patch != other.Patch) - return Patch.CompareTo(other.Patch); - - // Compare subpatch number - return Subpatch.CompareTo(other.Subpatch); - - } - - public override bool Equals(object? obj) - { - if (obj is null || !this.GetType().Equals(obj.GetType())) - { - return false; - } - - return this.Equals(obj); - // NOTE: For the purposes of a version number same values == same version - } - - public override int GetHashCode() - { - return Major + Minor + Build + Patch + Subpatch; - // NOTE: For the purposes of a version number same values == same version - // That is why this "hash" code will collide given two "different" versions with the same values - } - - public bool Equals(Version? other) - { - if (other is null) - { - return false; - } - return this.CompareTo(other) == 0; - } - - public static bool operator >(Version v1, Version v2) - { - return v1.CompareTo(v2) > 0; - } - - public static bool operator <(Version v1, Version v2) - { - return v1.CompareTo(v2) < 0; - } - - - public static bool operator >=(Version v1, Version v2) - { - return v1.CompareTo(v2) >= 0; - } - - public static bool operator <=(Version v1, Version v2) - { - return v1.CompareTo(v2) <= 0; - } -} diff --git a/Version/Version.IComparable.cs b/Version/Version.IComparable.cs new file mode 100644 index 0000000..320a468 --- /dev/null +++ b/Version/Version.IComparable.cs @@ -0,0 +1,75 @@ +namespace EdgeInstaller; + +public sealed partial class Version : IComparable +{ + public int CompareTo(Version? other) + { + if (other is null) + { + return 1; + } + + // full Equality is easy since everything should be the same. + // OPTIM: Doing this here is techinally more effecient than baking it in to the later code + // Bools and shit + if (this.Major == other.Major && + this.Minor == other.Minor && + this.Build == other.Build && + this.Patch == other.Patch && + this.Subpatch == other.Subpatch) + { + return 0; + } + + // Compare major version + if (Major != other.Major) + return Major.CompareTo(other.Major); + + // Compare minor version + if (Minor != other.Minor) + return Minor.CompareTo(other.Minor); + + // Compare build number + if (Build != other.Build) + return Build.CompareTo(other.Build); + + // Compare patch number + if (Patch != other.Patch) + return Patch.CompareTo(other.Patch); + + // Compare subpatch number + return Subpatch.CompareTo(other.Subpatch); + + } + + public bool Equals(Version? other) + { + if (other is null) + { + return false; + } + return this.CompareTo(other) == 0; + } + + public static bool operator >(Version v1, Version v2) + { + return v1.CompareTo(v2) > 0; + } + + public static bool operator <(Version v1, Version v2) + { + return v1.CompareTo(v2) < 0; + } + + + public static bool operator >=(Version v1, Version v2) + { + return v1.CompareTo(v2) >= 0; + } + + public static bool operator <=(Version v1, Version v2) + { + return v1.CompareTo(v2) <= 0; + } + +} diff --git a/Version/Version.IEquatable.cs b/Version/Version.IEquatable.cs new file mode 100644 index 0000000..72d5cf5 --- /dev/null +++ b/Version/Version.IEquatable.cs @@ -0,0 +1,22 @@ +namespace EdgeInstaller; + +public sealed partial class Version : IEquatable +{ + public override bool Equals(object? obj) + { + if (obj is null || !this.GetType().Equals(obj.GetType())) + { + return false; + } + + return this.Equals(obj); + // NOTE: For the purposes of a version number same values == same version + } + + public override int GetHashCode() + { + return Major + Minor + Build + Patch + Subpatch; + // NOTE: For the purposes of a version number same values == same version + // That is why this "hash" code will collide given two "different" versions with the same values + } +} diff --git a/Version/Version.cs b/Version/Version.cs new file mode 100644 index 0000000..565ed66 --- /dev/null +++ b/Version/Version.cs @@ -0,0 +1,41 @@ +using Serilog; +// Make a version since it is so much easier to compare when the class handles it. + +namespace EdgeInstaller; + +sealed partial class Version : IEquatable +{ + public int Major { get; private set; } + public int Minor { get; private set; } + public int Build { get; private set; } + public int Patch { get; private set; } + public int Subpatch { get; private set; } + + + public Version(string versionString) + { + Log.Information("Creating version from string {versionString}", versionString); + if (versionString == "") + { + throw new ArgumentException("The passed version string was empty", "versionString"); + } + + var splitVersion = versionString.Split('.'); + Log.Debug("SplitVersion = {@splitversion}", splitVersion); + Major = int.Parse(splitVersion[0]); + Minor = int.Parse(splitVersion[1]); + Build = int.Parse(splitVersion[2]); + + var splitPatch = splitVersion[3].Split('-'); + Log.Debug("SplitPatch = {@splitPatch}", splitPatch); + Log.Debug("SplitPatch values : [0] = {0}, [1] = {1}", splitPatch[0], splitPatch[1]); + Patch = int.Parse(splitPatch[0]); + Subpatch = int.Parse(splitPatch[1]); + Log.Debug("Version Created: {@version}", this); + } + + public override string ToString() + { + return $"{Major}.{Minor}.{Build}.{Patch}-{Subpatch}"; + } +} diff --git a/edge-install.csproj b/edge-install.csproj index 50daf6c..edd0b22 100644 --- a/edge-install.csproj +++ b/edge-install.csproj @@ -9,6 +9,7 @@ +