diff --git a/Src/GBX.NET.Tool.CLI/ArgsResolver.cs b/Src/GBX.NET.Tool.CLI/ArgsResolver.cs new file mode 100644 index 000000000..4c0723abf --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/ArgsResolver.cs @@ -0,0 +1,71 @@ +using GBX.NET.Tool.CLI.Inputs; + +namespace GBX.NET.Tool.CLI; + +internal sealed class ArgsResolver +{ + private readonly string[] args; + private readonly HttpClient client; + + public bool HasArgs => args.Length > 0; + + public ArgsResolver(string[] args, HttpClient http) + { + this.args = args; + this.client = http; + } + + public ToolConfiguration Resolve() + { + if (!HasArgs) + { + return new(); + } + + var inputs = new List(); + + var argsEnumerator = args.GetEnumerator(); + + while (argsEnumerator.MoveNext()) + { + var arg = argsEnumerator.Current as string ?? string.Empty; + + if (arg.StartsWith('-')) + { + continue; + } + + // - check http:// and https:// for URLs + // - check for individual files and files in zip archives + // - check for folders + // - check for stdin (maybe?) + // - check for configured user data path + if (Directory.Exists(arg)) + { + inputs.Add(new DirectoryInput(arg)); + continue; + } + + if (File.Exists(arg)) + { + inputs.Add(new FileInput(arg)); + continue; + } + + if (Uri.TryCreate(arg, UriKind.Absolute, out var uri)) + { + if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) + { + inputs.Add(new UriInput(client, uri)); + } + + continue; + } + } + + return new ToolConfiguration + { + Inputs = inputs + }; + } +} diff --git a/Src/GBX.NET.Tool.CLI/ConsoleOptions.cs b/Src/GBX.NET.Tool.CLI/ConsoleOptions.cs new file mode 100644 index 000000000..68c714b2a --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/ConsoleOptions.cs @@ -0,0 +1,6 @@ +namespace GBX.NET.Tool.CLI; + +public sealed class ConsoleOptions +{ + public bool DisableUpdateCheck { get; set; } +} diff --git a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj index 0eeb36c00..c0956c8cd 100644 --- a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj +++ b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj @@ -12,6 +12,7 @@ + diff --git a/Src/GBX.NET.Tool.CLI/Inputs/DirectoryInput.cs b/Src/GBX.NET.Tool.CLI/Inputs/DirectoryInput.cs new file mode 100644 index 000000000..9b47c4c1e --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/Inputs/DirectoryInput.cs @@ -0,0 +1,27 @@ + +using GBX.NET.Exceptions; + +namespace GBX.NET.Tool.CLI.Inputs; + +internal sealed record DirectoryInput(string DirectoryPath) : Input +{ + public override async Task ResolveAsync(CancellationToken cancellationToken) + { + var files = Directory.GetFiles(DirectoryPath, "*.*", SearchOption.AllDirectories); + + var tasks = files.Select>(async file => + { + try + { + await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await Gbx.ParseAsync(stream, cancellationToken: cancellationToken); + } + catch (NotAGbxException) + { + return await File.ReadAllBytesAsync(file, cancellationToken); + } + }); + + return await Task.WhenAll(tasks); + } +} diff --git a/Src/GBX.NET.Tool.CLI/Inputs/FileInput.cs b/Src/GBX.NET.Tool.CLI/Inputs/FileInput.cs new file mode 100644 index 000000000..9ab93ce2b --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/Inputs/FileInput.cs @@ -0,0 +1,20 @@ + +using GBX.NET.Exceptions; + +namespace GBX.NET.Tool.CLI.Inputs; + +internal sealed record FileInput(string FilePath) : Input +{ + public override async Task ResolveAsync(CancellationToken cancellationToken) + { + try + { + await using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await Gbx.ParseAsync(stream, cancellationToken: cancellationToken); + } + catch (NotAGbxException) + { + return File.ReadAllBytesAsync(FilePath, cancellationToken); + } + } +} diff --git a/Src/GBX.NET.Tool.CLI/Inputs/Input.cs b/Src/GBX.NET.Tool.CLI/Inputs/Input.cs new file mode 100644 index 000000000..68ed465e6 --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/Inputs/Input.cs @@ -0,0 +1,7 @@ + +namespace GBX.NET.Tool.CLI.Inputs; + +internal abstract record Input +{ + public abstract Task ResolveAsync(CancellationToken cancellationToken); +} diff --git a/Src/GBX.NET.Tool.CLI/Inputs/UriInput.cs b/Src/GBX.NET.Tool.CLI/Inputs/UriInput.cs new file mode 100644 index 000000000..807d2d6f9 --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/Inputs/UriInput.cs @@ -0,0 +1,24 @@ + +using GBX.NET.Exceptions; + +namespace GBX.NET.Tool.CLI.Inputs; + +internal sealed record UriInput(HttpClient Http, Uri Uri) : Input +{ + public override async Task ResolveAsync(CancellationToken cancellationToken) + { + using var response = await Http.GetAsync(Uri, cancellationToken); + + response.EnsureSuccessStatusCode(); + + try + { + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + return await Gbx.ParseAsync(stream, cancellationToken: cancellationToken); + } + catch (NotAGbxException) + { + return await response.Content.ReadAsByteArrayAsync(cancellationToken); + } + } +} diff --git a/Src/GBX.NET.Tool.CLI/ToolConfiguration.cs b/Src/GBX.NET.Tool.CLI/ToolConfiguration.cs new file mode 100644 index 000000000..18b4f9711 --- /dev/null +++ b/Src/GBX.NET.Tool.CLI/ToolConfiguration.cs @@ -0,0 +1,9 @@ +using GBX.NET.Tool.CLI.Inputs; + +namespace GBX.NET.Tool.CLI; + +internal sealed class ToolConfiguration +{ + public ConsoleOptions ConsoleOptions { get; init; } = new(); + public IReadOnlyCollection Inputs { get; init; } = []; +} diff --git a/Src/GBX.NET.Tool.CLI/ToolConsole.cs b/Src/GBX.NET.Tool.CLI/ToolConsole.cs index 730c3e7e6..85f46f0cb 100644 --- a/Src/GBX.NET.Tool.CLI/ToolConsole.cs +++ b/Src/GBX.NET.Tool.CLI/ToolConsole.cs @@ -1,7 +1,10 @@ -using GBX.NET.Tool.CLI.Exceptions; +using GBX.NET.LZO; +using GBX.NET.Tool.CLI.Exceptions; +using GBX.NET.Tool.CLI.Inputs; using Microsoft.Extensions.Logging; using Spectre.Console; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace GBX.NET.Tool.CLI; @@ -16,6 +19,11 @@ public ToolConsole(string[] args, HttpClient http) this.http = http; } + static ToolConsole() + { + Gbx.LZO = new Lzo(); + } + public static async Task> RunAsync(string[] args) { using var http = new HttpClient(); @@ -32,37 +40,38 @@ public static async Task> RunAsync(string[] args) catch (ConsoleProblemException ex) { AnsiConsole.MarkupInterpolated($"[yellow]{ex.Message}[/]"); - AnsiConsole.WriteLine(); - AnsiConsole.Markup("Press any key to continue..."); - Console.ReadKey(true); } catch (OperationCanceledException) { AnsiConsole.Markup("[yellow]Operation canceled.[/]"); - AnsiConsole.WriteLine(); - AnsiConsole.Markup("Press any key to continue..."); - Console.ReadKey(true); } catch (Exception ex) { AnsiConsole.WriteException(ex); - AnsiConsole.WriteLine(); - AnsiConsole.Markup("Press any key to continue..."); - Console.ReadKey(true); } + AnsiConsole.WriteLine(); + AnsiConsole.Markup("Press any key to continue..."); + Console.ReadKey(true); + return new ToolConsoleRunResult(tool); } private async Task RunAsync(CancellationToken cancellationToken) { + var argsResolver = new ArgsResolver(args, http); + var toolConfig = argsResolver.Resolve(); + // Request update info and additional stuff - var updateChecker = ToolUpdateChecker.Check(http); + var updateChecker = toolConfig.ConsoleOptions.DisableUpdateCheck + ? null + : ToolUpdateChecker.Check(http); await IntroWriter.WriteIntroAsync(args); // Check for updates here if received. If not, check at the end of the tool execution - var updateCheckCompleted = await updateChecker.TryCompareVersionAsync(cancellationToken); + var updateCheckCompleted = updateChecker is null + || await updateChecker.TryCompareVersionAsync(cancellationToken); AnsiConsole.WriteLine(); @@ -73,16 +82,107 @@ private async Task RunAsync(CancellationToken cancellationToken) // See what the tool can do var toolFunctionality = ToolFunctionalityResolver.Resolve(logger); - // Read the files from the arguments - // Quickly invalidate ones that do not meet functionality + var resolvedInputsDict = new Dictionary(); + var pickedCtor = default(ConstructorInfo); + var paramsForCtor = new List(); + + foreach (var constructor in toolFunctionality.Constructors) + { + var isInvalidCtor = false; + var parameters = constructor.GetParameters(); + + if (parameters.Length == 0) + { + // inputless tools? + continue; + } + + // issue here is with multiple inputs where it should repeat tool constructors + var inputEnumerator = toolConfig.Inputs.GetEnumerator(); + if (!inputEnumerator.MoveNext()) + { + continue; + } + var input = inputEnumerator.Current; + + foreach (var parameter in parameters) + { + var type = parameter.ParameterType; + + if (type == typeof(ILogger)) + { + paramsForCtor.Add(logger); + continue; + } + + if (type == typeof(Gbx)) + { + var resolvedObject = await input.ResolveAsync(cancellationToken); + + if (resolvedObject is not Gbx) + { + isInvalidCtor = true; + break; + } + + paramsForCtor.Add(resolvedObject); + continue; + } + + if (!type.IsGenericType) + { + continue; + } + + var typeDef = type.GetGenericTypeDefinition(); + + if (typeDef == typeof(Gbx<>)) + { + var nodeType = type.GetGenericArguments()[0]; + + var resolvedObject = await input.ResolveAsync(cancellationToken); + + if (resolvedObject is not Gbx gbx || gbx.Node?.GetType() != nodeType) + { + isInvalidCtor = true; + break; + } + + paramsForCtor.Add(resolvedObject); + continue; + } + + if (typeDef == typeof(IEnumerable<>)) + { + var elementType = type.GetGenericArguments()[0]; + continue; + } + } + + if (isInvalidCtor) + { + paramsForCtor.Clear(); + } + else + { + pickedCtor = constructor; + break; + } + } // Check again for updates if not done before - if (!updateCheckCompleted) + if (!updateCheckCompleted && updateChecker is not null) { updateCheckCompleted = await updateChecker.TryCompareVersionAsync(cancellationToken); } // Instantiate the tool + if (pickedCtor is null) + { + throw new ConsoleProblemException("Invalid files passed to the tool."); + } + + var toolInstance = pickedCtor.Invoke(paramsForCtor.ToArray()); // Run all produce methods in parallel and run mutate methods in sequence diff --git a/Src/GBX.NET.Tool/ToolFunctionality.cs b/Src/GBX.NET.Tool/ToolFunctionality.cs index 835689e49..270c771d2 100644 --- a/Src/GBX.NET.Tool/ToolFunctionality.cs +++ b/Src/GBX.NET.Tool/ToolFunctionality.cs @@ -1,6 +1,8 @@ -namespace GBX.NET.Tool; +using System.Reflection; + +namespace GBX.NET.Tool; public sealed class ToolFunctionality where T : ITool { - public required object?[] InputParameters { get; init; } + public required ConstructorInfo[] Constructors { get; init; } } diff --git a/Src/GBX.NET.Tool/ToolFunctionalityResolver.cs b/Src/GBX.NET.Tool/ToolFunctionalityResolver.cs index 4d458d731..05826a9a5 100644 --- a/Src/GBX.NET.Tool/ToolFunctionalityResolver.cs +++ b/Src/GBX.NET.Tool/ToolFunctionalityResolver.cs @@ -16,13 +16,11 @@ public static ToolFunctionality Resolve(ILogger logger) ResolveInterfaces(type); - var paramObjects = ResolveConstructors(type, logger); - logger.LogInformation("Tool properties resolved successfully."); return new ToolFunctionality { - InputParameters = paramObjects + Constructors = type.GetConstructors() }; } @@ -56,43 +54,4 @@ private static void ResolveInterfaces(Type type) } } } - - private static object?[] ResolveConstructors(Type type, ILogger logger) - { - foreach (var ctor in type.GetConstructors()) - { - var parameters = ctor.GetParameters(); - - if (parameters.Length == 0) - { - // input-less tools? - continue; - } - - var ctorUsable = true; - var paramObjects = new object?[parameters.Length]; - - // Check for constructor parameters - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - - if (parameter.ParameterType == typeof(ILogger)) - { - paramObjects[i] = logger; - continue; - } - - ctorUsable = false; - break; - } - - if (ctorUsable) - { - return paramObjects; - } - } - - return []; - } }