diff --git a/src/UnityBuildRunner.Core/DefaultBuilder.cs b/src/UnityBuildRunner.Core/DefaultBuilder.cs index b925e86..1af3f4f 100644 --- a/src/UnityBuildRunner.Core/DefaultBuilder.cs +++ b/src/UnityBuildRunner.Core/DefaultBuilder.cs @@ -58,6 +58,7 @@ public async Task BuildAsync(CancellationToken ct = default) logger.LogInformation($" - Command: {settings.UnityPath} {settings.ArgumentString}"); logger.LogInformation($" - WorkingDir: {settings.WorkingDirectory}"); logger.LogInformation($" - LogFilePath: {settings.LogFilePath}"); + logger.LogInformation($" - Timeout: {settings.TimeOut}"); var sw = Stopwatch.StartNew(); using var process = Process.Start(new ProcessStartInfo() { diff --git a/src/UnityBuildRunner.Core/DefaultSettings.cs b/src/UnityBuildRunner.Core/DefaultSettings.cs index 0c8d6e2..47b403b 100644 --- a/src/UnityBuildRunner.Core/DefaultSettings.cs +++ b/src/UnityBuildRunner.Core/DefaultSettings.cs @@ -92,7 +92,7 @@ public CancellationTokenSource CreateCancellationTokenSource(CancellationToken? /// /// Parse args and create . /// - public static bool TryParse(string[] args, string unityPath, TimeSpan timeout, [NotNullWhen(true)] out DefaultSettings? settings) + public static bool TryParse(IReadOnlyList args, string unityPath, TimeSpan timeout, [NotNullWhen(true)] out DefaultSettings? settings) { try { @@ -128,7 +128,7 @@ public static DefaultSettings Parse(IReadOnlyList args, string unityPath } var arguments = args.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); - var argumentString = string.Join(" ", arguments.Select(s => s.First() == '-' ? s : "\"" + s + "\"")); + var argumentString = string.Join(" ", arguments.Select(s => s.AsSpan()[0] == '-' ? s : QuoteString(s))); // WorkingDirectory should be cli launch path. var workingDirectory = Directory.GetCurrentDirectory(); @@ -142,6 +142,56 @@ public static DefaultSettings Parse(IReadOnlyList args, string unityPath return settings; } + /// + /// QuoteString when possible. + /// + /// + /// + /// + internal static string QuoteString(string text) + { + var span = text.AsSpan(); + + // `` is invalid + if (span.Length == 0) + { + throw new ArgumentException($"Argument is empty and is not valid string to quote. input: {text}"); + } + + // `"` is invalid + if (span.Length == 1 && span[0] == '"') + { + throw new ArgumentException($"Argument is \" and is not valid string to quote. input: {text}"); + } + + // `"foo` is invalid + if (span.Length >= 2 && span[0] == '"' && span[^1] != '"') + { + throw new ArgumentException($"Argument begin with \" but not closed, please complete quote. input: {text}"); + } + + // `foo"` is invalid + if (span.Length >= 2 && span[0] != '"' && span[^1] == '"') + { + throw new ArgumentException($"Argument end with \" but not begin with \", please complete quote. input: {text}"); + } + + // `foo"foo` or `foo"foo"foo` is invalid + if (span[1..^1].Contains('"')) + { + throw new ArgumentException($"Argument contains \", but is invalid. input: {text}"); + } + + // `""` and `"foo"` is valid + if (span.Length >= 2 && span[0] == '"' && span[^1] == '"') + { + return text; + } + + // `foo` is valid + return $"\"{text}\""; + } + /// /// Parse `-logFile ` from arguments. /// @@ -161,6 +211,11 @@ internal static string ParseLogFile(IReadOnlyList args) return logFile; } + /// + /// Detect Logfile name is valid or not + /// + /// + /// internal static bool IsValidLogFileName(string? fileName) { // missing filename is not valid diff --git a/src/UnityBuildRunner/Program.cs b/src/UnityBuildRunner/Program.cs index 42f0da2..653ffca 100644 --- a/src/UnityBuildRunner/Program.cs +++ b/src/UnityBuildRunner/Program.cs @@ -31,22 +31,20 @@ public UnityBuildRunnerCommand(ILogger logger) { arguments = arguments.Except(new[] { "--unity-path", unityPath }); } + var args = arguments?.ToArray(); - if (arguments is not null && arguments.Any()) + if (args is null || !args.Any()) { - var timeoutSpan = TimeSpan.TryParse(timeout, out var r) ? r : timeoutDefault; - var settings = DefaultSettings.Parse(arguments.ToArray()!, unityPath, timeoutSpan); - using var cts = settings.CreateCancellationTokenSource(Context.CancellationToken); - - // build - var builder = new DefaultBuilder(settings, logger); - await builder.BuildAsync(cts.Token); - return builder.ExitCode; - } - else - { - logger.LogError($"No valid argument found, exiting. You have specified arguments: {string.Join(" ", arguments?.ToArray() ?? Array.Empty())}"); - return 1; + throw new ArgumentOutOfRangeException($"No valid argument found, exiting. You have specified arguments: {string.Join(" ", args ?? Array.Empty())}"); } + + // build + var timeoutSpan = TimeSpan.TryParse(timeout, out var r) ? r : timeoutDefault; + var settings = DefaultSettings.Parse(args!, unityPath, timeoutSpan); + using var cts = settings.CreateCancellationTokenSource(Context.CancellationToken); + + var builder = new DefaultBuilder(settings, logger); + await builder.BuildAsync(cts.Token); + return builder.ExitCode; } } diff --git a/src/UnityBuildRunner/Properties/launchSettings.json b/src/UnityBuildRunner/Properties/launchSettings.json index 96ec211..22866a5 100644 --- a/src/UnityBuildRunner/Properties/launchSettings.json +++ b/src/UnityBuildRunner/Properties/launchSettings.json @@ -18,6 +18,18 @@ "UnityBuildRunner (-logFile -)": { "commandName": "Project", "commandLineArgs": "--unity-path \"C:/Program Files/Unity/Hub/Editor/2021.3.17f1/Editor/Unity.exe\" -quit -batchmode -nographics -silent-crashes -logfile - -buildTarget StandaloneWindows64 -projectPath ../../../../../sandbox/Sandbox.Unity -executeMethod BuildUnity.BuildGame" + }, + "UnityBuildRunner (quote-slash)": { + "commandName": "Project", + "commandLineArgs": "--unity-path \"C:/Program Files/Unity/Hub/Editor/2021.3.17f1/Editor/Unity.exe\" -quit -batchmode -nographics -silent-crashes -logfile - -buildTarget StandaloneWindows64 -projectPath \"../../../../../sandbox/Sandbox.Unity/\" -executeMethod BuildUnity.BuildGame" + }, + "UnityBuildRunner (quote)": { + "commandName": "Project", + "commandLineArgs": "--unity-path \"C:/Program Files/Unity/Hub/Editor/2021.3.17f1/Editor/Unity.exe\" -quit -batchmode -nographics -silent-crashes -logfile - -buildTarget StandaloneWindows64 -projectPath \"..\\..\\..\\..\\..\\sandbox/Sandbox.Unity\\\\\" -executeMethod BuildUnity.BuildGame" + }, + "UnityBuildRunner (quote-bad)": { + "commandName": "Project", + "commandLineArgs": "--unity-path \"C:/Program Files/Unity/Hub/Editor/2021.3.17f1/Editor/Unity.exe\" -quit -batchmode -nographics -silent-crashes -logfile - -buildTarget StandaloneWindows64 -projectPath \"..\\..\\..\\..\\..\\sandbox/Sandbox.Unity\\\" -executeMethod BuildUnity.BuildGame" } } } diff --git a/tests/UnityBuildRunner.Core.Tests/SettingsUnitTests.cs b/tests/UnityBuildRunner.Core.Tests/SettingsUnitTests.cs index da6b3b7..e2d1517 100644 --- a/tests/UnityBuildRunner.Core.Tests/SettingsUnitTests.cs +++ b/tests/UnityBuildRunner.Core.Tests/SettingsUnitTests.cs @@ -93,9 +93,13 @@ public void ArgsShouldNotContainNullOrWhiteSpace(string[] actual, string[] expec [InlineData(new[] { "-bathmode", "", "-nographics", "-projectpath", "HogemogeProject", "-executeMethod", "MethodName", "-quite", "-logfile", "build.log" }, "-bathmode -nographics -projectpath \"HogemogeProject\" -executeMethod \"MethodName\" -quite -logfile \"build.log\"")] [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", "HogemogeProject", "-executeMethod", "MethodName", "-quite", " " }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"HogemogeProject\" -executeMethod \"MethodName\" -quite")] [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", " ", "HogemogeProject", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"HogemogeProject\" -executeMethod \"MethodName\" -quite")] + [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", "foo/bar/baz", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo/bar/baz\" -executeMethod \"MethodName\" -quite")] // -projectpath ends without / [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", "foo/bar/baz/", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo/bar/baz/\" -executeMethod \"MethodName\" -quite")] - [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", @"foo\bar\baz", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\" -executeMethod \"MethodName\" -quite")] + [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", @"foo\bar\baz", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\" -executeMethod \"MethodName\" -quite")] // -projectpath ends without \ [InlineData(new[] { "-logfile", "hoge.log", "-bathmode", "-nographics", "-projectpath", @"foo\bar\baz\", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\\\" -executeMethod \"MethodName\" -quite")] + [InlineData(new[] { "-logfile", "\"hoge.log\"", "-bathmode", "-nographics", "-projectpath", @"""foo\bar\baz\""", "-executeMethod", "\"MethodName\"", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\\\" -executeMethod \"MethodName\" -quite")] // input is already quoted + [InlineData(new[] { "-logfile", "\"hoge.log\"", "-bathmode", "-nographics", "-projectpath", @"""foo\bar\baz\""", "-executeMethod", "MethodName", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\\\" -executeMethod \"MethodName\" -quite")] // input is already quoted + [InlineData(new[] { "-logfile", "\"hoge.log\"", "-bathmode", "-nographics", "-projectpath", @"""foo\bar\baz""", "-executeMethod", "\"MethodName\"", "-quite" }, "-logfile \"hoge.log\" -bathmode -nographics -projectpath \"foo\\bar\\baz\" -executeMethod \"MethodName\" -quite")] // input is already quoted public void ArgsumentStringShouldFormated(string[] actual, string expected) { ISettings settings = DefaultSettings.Parse(actual, _unityPath, _timeout); @@ -112,4 +116,27 @@ public void IsValidLogFilePath(string? logFilePath, bool expected) { DefaultSettings.IsValidLogFileName(logFilePath).Should().Be(expected); } + + [Theory] + [InlineData("")] + [InlineData("\"")] + [InlineData("\"foo")] + [InlineData("foo\"")] + [InlineData("fo\"o")] + [InlineData("f\"o\"o")] + [InlineData("\"fo\"o\"")] + [InlineData("\"f\"o\"o\"")] + public void QuoteStringInvalidInput(string input) + { + Assert.Throws(() => DefaultSettings.QuoteString(input)); + } + + [Theory] + [InlineData("\"\"", "\"\"")] + [InlineData("\"foo\"", "\"foo\"")] + [InlineData("foo", "\"foo\"")] + public void QuoteStringValidValue(string input, string expected) + { + DefaultSettings.QuoteString(input).Should().Be(expected); + } }