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