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.
This commit is contained in:
Robert Morrison 2023-06-08 04:36:31 +01:00
parent f650e6a3c9
commit 60e06b2144
Signed by: robert
GPG Key ID: 73E012EB3F4EC696
16 changed files with 827 additions and 533 deletions

View File

@ -1,60 +0,0 @@
// Experimental code to open and dissect a `.deb` archive
static class DebArchive
{
const string DPKG_AR_MAGIC = "!<arch>\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
*/

145
DebUnpack.cs Normal file
View File

@ -0,0 +1,145 @@
using System.Text;
using Serilog;
namespace EdgeInstaller;
sealed class DebFile : IDisposable
{
const int HeaderBytes = 60;
const string FileMagic = "!<arch>\n";
private MemoryStream _fileStream;
public List<DebEntry> 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");
}
}
}

20
Installer/Installer.cs Normal file
View File

@ -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)
{
}
}

View File

@ -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;
}
}

View File

@ -1,9 +1,9 @@
using Serilog; using Serilog;
using System.Text.Json.Serialization;
// Defines a Package // Defines a Package
namespace EdgeInstaller;
sealed class Package sealed partial class Package
{ {
public string PackageName { get; private set; } public string PackageName { get; private set; }
@ -12,16 +12,6 @@ sealed class Package
public string SHA256 { get; private set; } public string SHA256 { get; private set; }
public string Filename { 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) public Package(string packageString)
{ {
Log.Information("Creating package from string"); Log.Information("Creating package from string");
@ -61,6 +51,4 @@ sealed class Package
/* TODO: capture dependencies and create method to find them. /* TODO: capture dependencies and create method to find them.
- This would probably need distro specific lookup tables. - This would probably need distro specific lookup tables.
- Also would ideally need code to shell out (at root level)
to perform installs...
*/ */

126
Program.Downloaders.cs Normal file
View File

@ -0,0 +1,126 @@
using Spectre.Console;
using Serilog;
namespace EdgeInstaller;
public static partial class EdgeInstall
{
private static async Task<string> 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<string> 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<string> 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<MemoryStream> 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;
}
}

View File

@ -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");
}
}

150
Program.Renderers.cs Normal file
View File

@ -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<Package> 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);
}
});
}
}

View File

@ -1,15 +1,22 @@
//using System.CommandLine; //using System.CommandLine;
using Serilog; using Serilog;
using Spectre.Console; using Spectre.Console;
using System.IO.Compression;
using System.Text.Json; using System.Text.Json;
using System.Security.Cryptography;
using System.Text; using System.Text;
namespace EdgeInstaller; 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 HttpClient _client = new HttpClient();
private static readonly string DataDir = private static readonly string DataDir =
@ -18,23 +25,19 @@ public static class EdgeInstall
"ClosedLess", "ClosedLess",
"EdgeInstall-Jankmode"); "EdgeInstall-Jankmode");
public static async Task<int> Main() public static async Task<int> Main()
{ {
// WARN: Hardcoded variables are bad m'kay... _client.BaseAddress = new Uri("https://packages.microsoft.com/repos/edge/");
string Arch = "amd64";
// Ensure DataDir exists. // Ensure DataDir exists.
// NOTE: Directory.CreateDirectory will ALWAYS create the entire path // NOTE: Directory.CreateDirectory will ALWAYS create the entire path
Directory.CreateDirectory(DataDir); Directory.CreateDirectory(DataDir);
// TODO: wrap this code in foot detection since foot isn't detected properly BUGFIX_ansiDetection();
// 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;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .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(); .CreateLogger();
var Release = AnsiConsole.Status() var Release = AnsiConsole.Status()
@ -49,26 +52,26 @@ public static class EdgeInstall
Log.Debug("{rs}", ReleaseString); Log.Debug("{rs}", ReleaseString);
AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]"); AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]");
ReleaseData data = getReleaseData(ReleaseString); ReleaseData data = new(ReleaseString);
RenderReleaseTable(data); RenderReleaseTable(data);
Arch = AnsiConsole.Prompt( // only select arch if needed
new SelectionPrompt<string>() string TargetArch = data.Architectures.Count > 1 ? AnsiConsole.Prompt(
.Title("Choose Architecture") new SelectionPrompt<string>()
.PageSize(10) .Title("Choose Architecture")
.MoreChoicesText("[grey]Scroll for more...[/]") .PageSize(5)
.AddChoices(data.Architectures) .MoreChoicesText("[grey]Scroll for more...[/]")
); .AddChoices(data.Architectures)
) : data.Architectures.First();
// create a directory for the chosen architecture // create a directory for the chosen architecture
// NOTE: we also grab the path here so we can use it later. // 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. // 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 // NOTE: we only do this if our chosen architecture hasn't been downloaded
// OOF semaphores..
bool NeedDownload = true; bool NeedDownload = true;
Log.Information("Reached Package download Stage"); Log.Information("Reached Package download Stage");
@ -98,34 +101,34 @@ public static class EdgeInstall
.Where( .Where(
file => file =>
file.filename.Contains(".gz") && 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(); .First();
if (gzFile is null) if (gzFile is null)
{ {
throw new NullReferenceException("OOPS gzfile 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() string filename = await AnsiConsole.Progress()
.Columns(new ProgressColumn[] .Columns(new ProgressColumn[]
{ {
new TaskDescriptionColumn(), new TaskDescriptionColumn(),
new ProgressBarColumn(), new ProgressBarColumn(),
new PercentageColumn(), new PercentageColumn(),
new RemainingTimeColumn(), new RemainingTimeColumn(),
new SpinnerColumn(), new SpinnerColumn(),
}) })
.StartAsync<string>(async ctx => .StartAsync<string>(async ctx =>
{ {
var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false }); 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<ReleaseData>(data); string JSONdata = JsonSerializer.Serialize<ReleaseData>(data);
File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata); File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata);
if (!ValidateChecksum(filename, gzFile.Checksum)) if (!ValidateChecksum(new FileInfo(filename), gzFile.Checksum))
{ {
File.Delete(filename); File.Delete(filename);
Environment.Exit(1); Environment.Exit(1);
@ -145,6 +148,10 @@ public static class EdgeInstall
if (line == "") if (line == "")
{ {
string PackageString = sb.ToString(); 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. sb.Clear(); // ensure we clear the StringBuilder when we are done with it.
try try
@ -171,15 +178,15 @@ public static class EdgeInstall
.ThenByDescending(package => package.Version).ToList(); .ThenByDescending(package => package.Version).ToList();
// Print any packages we have // Print any packages we have
var Variant = AnsiConsole.Prompt( var Variant = AnsiConsole.Prompt(
new SelectionPrompt<string>() new SelectionPrompt<string>()
.Title("Choose variant") .Title("Choose variant")
.PageSize(4) .PageSize(4)
.AddChoices(new[] { .AddChoices(new[] {
"stable", "stable",
"beta", "beta",
"dev", "dev",
"all" "all"
}) })
); );
@ -191,19 +198,19 @@ public static class EdgeInstall
else else
{ {
VariantPackages = packages VariantPackages = packages
.Where(package => .Where(package =>
package.PackageName.Contains(Variant)) package.PackageName.Contains(Variant))
.ToList(); .ToList();
} }
RenderPackagesTable(VariantPackages); RenderPackagesTable(VariantPackages);
var Package = AnsiConsole.Prompt( var Package = AnsiConsole.Prompt(
new SelectionPrompt<string>() new SelectionPrompt<string>()
.Title("Choose Package to Install") .Title("Choose Package to Install")
.PageSize(5) .PageSize(5)
.AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct()) .AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct())
); );
var ChosenPackages = packages var ChosenPackages = packages
@ -215,261 +222,77 @@ public static class EdgeInstall
RenderPackageAsTable(LatestVersion); RenderPackageAsTable(LatestVersion);
return 0; var versionPath = LatestVersion.Filename
} .Replace("pool/main/m/", "")
.Replace(".deb", "");
static void RenderPackageAsTable(Package package) var versionsDir = Path.Join(Archdir, "Versions");
{ // Let's get and unpack the latest version
Table Package = new Table()
.Centered()
.Border(TableBorder.Rounded)
.HideHeaders()
.Title("Package To Install");
AnsiConsole.Live(Package) if (true)//!Directory.Exists(Path.Join(versionsDir, versionPath)))
.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<Package> 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}[/]"; using (MemoryStream debStream =
} await AnsiConsole.Progress()
if (name.Contains("beta")) .Columns(new ProgressColumn[]
{ {
return $"[yellow]{name}[/]"; new TaskDescriptionColumn(),
} new ProgressBarColumn(),
if (name.Contains("stable")) new PercentageColumn(),
{ new RemainingTimeColumn(),
return $"[green]{name}[/]"; new SpinnerColumn(),
} })
return name; .StartAsync<MemoryStream>(async ctx =>
}
static async Task<string> 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<string> 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<string> 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)
{ {
AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]"); var task = ctx.AddTask(LatestVersion.Filename, new ProgressTaskSettings { AutoStart = false });
break;
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; return 0;
}
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));
}
} }
} }
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: /*TODO:
- Parse package to find latest version - Check version
- Check version - Offer update
- Offer update - Make SENSIBLE backup
- Make SENSIBLE backup - Do install
- Do install = if install fails revert backup
= if install fails revert backup */
*/
} }

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EdgeInstaller;
public sealed partial class ReleaseData
{
[JsonConstructor]
public ReleaseData(DateTime date, List<string> architectures, List<PackageFile> packageFiles)
{
this.Date = date;
this.Architectures = architectures;
this.PackageFiles = packageFiles;
}
}

View File

@ -2,22 +2,15 @@
// Substring should be replaced with a .split(": ") // Substring should be replaced with a .split(": ")
// Also Directly assign to the values instead of using temporary variables. // Also Directly assign to the values instead of using temporary variables.
using Serilog; using Serilog;
using System.Text.Json.Serialization;
namespace EdgeInstaller; namespace EdgeInstaller;
sealed class ReleaseData public sealed partial class ReleaseData
{ {
public DateTime Date { get; private set; } public DateTime Date { get; private set; }
public List<string> Architectures { get; private set; } public List<string> Architectures { get; private set; }
public List<PackageFile> PackageFiles { get; private set; } public List<PackageFile> PackageFiles { get; private set; }
[JsonConstructor]
public ReleaseData(DateTime date, List<string> architectures, List<PackageFile> packageFiles)
{
this.Date = date;
this.Architectures = architectures;
this.PackageFiles = packageFiles;
}
public ReleaseData(string Release) public ReleaseData(string Release)
{ {
@ -30,31 +23,19 @@ sealed class ReleaseData
paramName: "Release"); paramName: "Release");
} }
Architectures = new();
PackageFiles = new();
// Pull release apart // Pull release apart
List<string> ReleaseLines = Release.Split("\n", StringSplitOptions.RemoveEmptyEntries).ToList(); List<string> ReleaseLines = Release.Split("\n", StringSplitOptions.RemoveEmptyEntries).ToList();
Log.Debug("ReleaseLines = {@releaselines}", ReleaseLines); Log.Debug("ReleaseLines = {@releaselines}", ReleaseLines);
// Safely find and parse the Date field Date = DateTime.Parse(
string DateLine = ReleaseLines.Where(
ReleaseLines.Where(
Line => Line.Contains("Date:")) Line => Line.Contains("Date:"))
.FirstOrDefault("").Substring(5); .First()
Log.Debug("DateLine = {DateLine}", DateLine); .Split(": ")
.Last()
);
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 // Safely find and add the possible architectures
string ArchitecturesLine = string ArchitecturesLine =
@ -67,19 +48,25 @@ sealed class ReleaseData
throw _lineFormatException("architectures"); throw _lineFormatException("architectures");
} }
Architectures = ArchitecturesLine Architectures = ArchitecturesLine
.Substring(ArchitecturesLine.IndexOf(":") + 1) // We only want items after the label .Split(": ").Last() // We only want items after the label
.Split(" ", StringSplitOptions.RemoveEmptyEntries) // Space delimited list .Split(" ", StringSplitOptions.RemoveEmptyEntries)
.ToList(); // Convert to the superior collection type .ToList();
Log.Debug("Extracted architectures {@architectures}", Architectures); 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 // First split the Lines to only get the file data
List<string> SHA256FileData = ReleaseLines.Where((value, index) => // NOTE: This is probably the cleanest way to do this.
index > ReleaseLines.IndexOf("SHA256:") && index < ReleaseLines.IndexOf("SHA512:") List<string> SHA256FileData = ReleaseLines.Where((_, index) =>
).ToList(); index > ReleaseLines.IndexOf("SHA256:") &&
index < ReleaseLines.IndexOf("SHA512:")
).ToList();
Log.Debug("SHA256FileData = {@data}", SHA256FileData); Log.Debug("SHA256FileData = {@data}", SHA256FileData);
// Now process these lines to get the data we need // 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) // 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 // 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) foreach (var file in emptyFiles)
{ {
releasePackageFiles.RemoveAll(package => releasePackageFiles
.RemoveAll(package =>
package.filename == file.filename || package.filename == file.filename ||
package.filename == $"{file.filename}.gz" package.filename == $"{file.filename}.gz"
); );
} }
PackageFiles = releasePackageFiles; 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 // 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()) 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) if (PackageFiles.Where(file => file.filename.Contains(arch)).Count() == 0)
{ {

View File

@ -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<Version>, IEquatable<Version>
{
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;
}
}

View File

@ -0,0 +1,75 @@
namespace EdgeInstaller;
public sealed partial class Version : IComparable<Version>
{
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;
}
}

View File

@ -0,0 +1,22 @@
namespace EdgeInstaller;
public sealed partial class Version : IEquatable<Version>
{
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
}
}

41
Version/Version.cs Normal file
View File

@ -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<Version>
{
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}";
}
}

View File

@ -9,6 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CST.RecursiveExtractor" Version="1.2.13" />
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Spectre.Console" Version="0.47.0" /> <PackageReference Include="Spectre.Console" Version="0.47.0" />