diff --git a/AffinityHook/Program.cs b/AffinityHook/Program.cs
index 22d16ec..6018939 100644
--- a/AffinityHook/Program.cs
+++ b/AffinityHook/Program.cs
@@ -91,8 +91,6 @@ static int Main(string[] args)
Console.WriteLine();
}
- var forwardedArgs = string.Join(" ", args.Select(a => a.Contains(" ") ? "\"" + a + "\"" : a));
-
Console.WriteLine($"Starting Affinity from: {affinityExe}");
Console.WriteLine($"Mode: {(detachMode ? "Detached" : "Attached")}");
@@ -100,7 +98,7 @@ static int Main(string[] args)
var startInfo = new ProcessStartInfo
{
FileName = affinityExe,
- Arguments = forwardedArgs,
+ Arguments = BuildArguments(args),
WorkingDirectory = affinityDir,
UseShellExecute = false,
CreateNoWindow = false
@@ -195,6 +193,64 @@ private static bool IsRunningOnWine()
}
}
+ private static readonly char[] ArgumentDelimiters = { ' ', '\t', '\n', '\v', '"' };
+
+ ///
+ /// Join arguments into a command line that CreateProcess will parse back
+ /// into the same arguments.
+ ///
+ ///
+ /// With UseShellExecute = false the Arguments string is handed to
+ /// CreateProcess and re-parsed using the CommandLineToArgvW rules, so an
+ /// unquoted path containing spaces arrives at the target as several
+ /// arguments. Affinity then reports "failed to open" once per fragment.
+ ///
+ private static string BuildArguments(string[] args)
+ {
+ var sb = new StringBuilder();
+ for (int i = 0; i < args.Length; i++)
+ {
+ if (i > 0) sb.Append(' ');
+ sb.Append(QuoteArgument(args[i]));
+ }
+ return sb.ToString();
+ }
+
+ ///
+ /// Quote a single argument per the CommandLineToArgvW rules.
+ ///
+ ///
+ /// Backslashes are only special immediately before a quote, which is why
+ /// a plain Replace("\"", "\\\"") is not sufficient: a Windows path
+ /// ending in a separator, such as C:\My Folder\, would otherwise have its
+ /// closing quote escaped by the trailing backslash and swallow the next
+ /// argument.
+ ///
+ private static string QuoteArgument(string arg)
+ {
+ if (arg.Length > 0 && arg.IndexOfAny(ArgumentDelimiters) < 0) return arg;
+
+ var sb = new StringBuilder(arg.Length + 2);
+ sb.Append('"');
+ for (int i = 0; ; i++)
+ {
+ int backslashes = 0;
+ while (i < arg.Length && arg[i] == '\\') { backslashes++; i++; }
+
+ if (i == arg.Length)
+ {
+ // double any trailing backslashes so they do not escape the closing quote
+ sb.Append('\\', backslashes * 2);
+ break;
+ }
+
+ if (arg[i] == '"') sb.Append('\\', backslashes * 2 + 1).Append('"');
+ else sb.Append('\\', backslashes).Append(arg[i]);
+ }
+ sb.Append('"');
+ return sb.ToString();
+ }
+
///
/// Inject using simple CreateRemoteThread + LoadLibrary
/// This works on both Windows and Wine - we always use the native bootstrap