Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escape paths in process arguments #6

Merged
merged 2 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/EphemeralMongo.Core.Tests/ProcessArgumentTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
3 changes: 3 additions & 0 deletions src/EphemeralMongo.Core/AssemblyProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("EphemeralMongo.Core.Tests")]
21 changes: 4 additions & 17 deletions src/EphemeralMongo.Core/FileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
28 changes: 19 additions & 9 deletions src/EphemeralMongo.Core/MongoRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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))
{
Expand Down
64 changes: 64 additions & 0 deletions src/EphemeralMongo.Core/ProcessArgument.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}