diff --git a/src/EphemeralMongo.Core.Tests/ProcessArgumentTests.cs b/src/EphemeralMongo.Core.Tests/ProcessArgumentTests.cs new file mode 100644 index 0000000..01c0778 --- /dev/null +++ b/src/EphemeralMongo.Core.Tests/ProcessArgumentTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace EphemeralMongo.Core.Tests; + +public class ProcessArgumentTests +{ + [Theory] + [InlineData("", "\"\"")] + [InlineData("/", "/")] + [InlineData("\\", "\\")] + [InlineData("foo", "foo")] + [InlineData("/foo", "/foo")] + [InlineData("c:\\foo", "c:\\foo")] + [InlineData("\\foo", "\\foo")] + [InlineData("foo bar", "\"foo bar\"")] + [InlineData("/foo/hello world", "\"/foo/hello world\"")] + [InlineData("c:\\foo\\hello world", "\"c:\\foo\\hello world\"")] + [InlineData("\\\"", "\"\\\\\\\"\"")] + [InlineData("fo\"ob\"a \\\\r", "\"fo\\\"ob\\\"a \\\\r\"")] + public void Nothing(string inputPath, string expectedEscapedPath) + { + Assert.Equal(expectedEscapedPath, ProcessArgument.Escape(inputPath)); + } +} \ No newline at end of file diff --git a/src/EphemeralMongo.Core/AssemblyProperties.cs b/src/EphemeralMongo.Core/AssemblyProperties.cs new file mode 100644 index 0000000..3e70d2a --- /dev/null +++ b/src/EphemeralMongo.Core/AssemblyProperties.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("EphemeralMongo.Core.Tests")] \ No newline at end of file diff --git a/src/EphemeralMongo.Core/FileSystem.cs b/src/EphemeralMongo.Core/FileSystem.cs index d8f2ca1..5493b3e 100644 --- a/src/EphemeralMongo.Core/FileSystem.cs +++ b/src/EphemeralMongo.Core/FileSystem.cs @@ -24,23 +24,10 @@ public void MakeFileExecutable(string path) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - var chmod = Process.Start("chmod", "+x " + path); - if (chmod != null) - { - try - { - chmod.WaitForExit(); - - if (chmod.ExitCode != 0) - { - throw new IOException($"Could not set executable bit for '{path}'"); - } - } - finally - { - chmod.Dispose(); - } - } + // Do not throw if exit code is not equal to zero. + // If there's something wrong with the path or permissions, we'll see it later. + using var chmod = Process.Start("chmod", "+x " + ProcessArgument.Escape(path)); + chmod?.WaitForExit(); } } } \ No newline at end of file diff --git a/src/EphemeralMongo.Core/MongoRunner.cs b/src/EphemeralMongo.Core/MongoRunner.cs index f9ff610..9e4fcc1 100644 --- a/src/EphemeralMongo.Core/MongoRunner.cs +++ b/src/EphemeralMongo.Core/MongoRunner.cs @@ -39,18 +39,26 @@ private IMongoRunner RunInternal() var executablePath = this._executableLocator.FindMongoExecutablePath(this._options, MongoProcessKind.Mongod); this._fileSystem.MakeFileExecutable(executablePath); - // Ensure data directory exists and has no existing MongoDB lock file + // Ensure data directory exists... this._dataDirectory = this._options.DataDirectory ?? Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); this._fileSystem.CreateDirectory(this._dataDirectory); - // https://stackoverflow.com/a/6857973/825695 - var lockFilePath = Path.Combine(this._dataDirectory, "mongod.lock"); - this._fileSystem.DeleteFile(lockFilePath); + try + { + // ...and has no existing MongoDB lock file + // https://stackoverflow.com/a/6857973/825695 + var lockFilePath = Path.Combine(this._dataDirectory, "mongod.lock"); + this._fileSystem.DeleteFile(lockFilePath); + } + catch + { + // Ignored - this data directory might already be in use, we'll see later how mongod reacts + } this._options.MongoPort = this._portFactory.GetRandomAvailablePort(); // Build MongoDB executable arguments - var arguments = string.Format(CultureInfo.InvariantCulture, "--dbpath \"{0}\" --port {1} --bind_ip 127.0.0.1", this._dataDirectory, this._options.MongoPort); + var arguments = string.Format(CultureInfo.InvariantCulture, "--dbpath {0} --port {1} --bind_ip 127.0.0.1", ProcessArgument.Escape(this._dataDirectory), this._options.MongoPort); arguments += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty : " --tlsMode disabled"; arguments += this._options.UseSingleNodeReplicaSet ? " --replSet " + this._options.ReplicaSetName : string.Empty; arguments += this._options.AdditionalArguments == null ? string.Empty : " " + this._options.AdditionalArguments; @@ -146,11 +154,12 @@ public void Import(string database, string collection, string inputFilePath, str } var executablePath = this._runner._executableLocator.FindMongoExecutablePath(this._runner._options, MongoProcessKind.MongoImport); + this._runner._fileSystem.MakeFileExecutable(executablePath); var arguments = string.Format( CultureInfo.InvariantCulture, - @"--uri=""{0}"" --db={1} --collection={2} --file=""{3}"" {4} {5}", - this.ConnectionString, database, collection, inputFilePath, drop ? " --drop" : string.Empty, additionalArguments ?? string.Empty); + @"--uri=""{0}"" --db={1} --collection={2} --file={3} {4} {5}", + this.ConnectionString, database, collection, ProcessArgument.Escape(inputFilePath), drop ? " --drop" : string.Empty, additionalArguments ?? string.Empty); using (var process = this._runner._processFactory.CreateMongoProcess(this._runner._options, MongoProcessKind.MongoImport, executablePath, arguments)) { @@ -182,11 +191,12 @@ public void Export(string database, string collection, string outputFilePath, st } var executablePath = this._runner._executableLocator.FindMongoExecutablePath(this._runner._options, MongoProcessKind.MongoExport); + this._runner._fileSystem.MakeFileExecutable(executablePath); var arguments = string.Format( CultureInfo.InvariantCulture, - @"--uri=""{0}"" --db={1} --collection={2} --out=""{3}"" {4}", - this.ConnectionString, database, collection, outputFilePath, additionalArguments ?? string.Empty); + @"--uri=""{0}"" --db={1} --collection={2} --out={3} {4}", + this.ConnectionString, database, collection, ProcessArgument.Escape(outputFilePath), additionalArguments ?? string.Empty); using (var process = this._runner._processFactory.CreateMongoProcess(this._runner._options, MongoProcessKind.MongoExport, executablePath, arguments)) { diff --git a/src/EphemeralMongo.Core/ProcessArgument.cs b/src/EphemeralMongo.Core/ProcessArgument.cs new file mode 100644 index 0000000..1c28986 --- /dev/null +++ b/src/EphemeralMongo.Core/ProcessArgument.cs @@ -0,0 +1,64 @@ +using System.Text; + +namespace EphemeralMongo; + +internal static class ProcessArgument +{ + private const char DoubleQuote = '"'; + private const char Backslash = '\\'; + + // Inspired from https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs + public static string Escape(string path) + { + // Path does not need to be escaped + if (path.Length != 0 && path.All(c => !char.IsWhiteSpace(c) && c != DoubleQuote)) + { + return path; + } + + var stringBuilder = new StringBuilder(); + + stringBuilder.Append(DoubleQuote); + + for (var i = 0; i < path.Length;) + { + var c = path[i++]; + + if (c == Backslash) + { + var backslashCount = 1; + while (i < path.Length && path[i] == Backslash) + { + backslashCount++; + i++; + } + + if (i == path.Length) + { + stringBuilder.Append(Backslash, backslashCount * 2); + } + else if (path[i] == DoubleQuote) + { + stringBuilder.Append(Backslash, backslashCount * 2 + 1).Append(DoubleQuote); + i++; + } + else + { + stringBuilder.Append(Backslash, backslashCount); + } + } + else if (c == DoubleQuote) + { + stringBuilder.Append(Backslash).Append(DoubleQuote); + } + else + { + stringBuilder.Append(c); + } + } + + stringBuilder.Append(DoubleQuote); + + return stringBuilder.ToString(); + } +} \ No newline at end of file