Skip to content

Commit fc2977b

Browse files
author
Bura Chuhadar
committed
Improving overall quality of the implementation.
1- Bringing CommandLineToArgvW to parse command registry from Win32API 2- Execution of the context menu was a problem for some applications. This has been fixed. 3- Improved MUIVerb support
1 parent 8c2a36d commit fc2977b

File tree

7 files changed

+180
-51
lines changed

7 files changed

+180
-51
lines changed

Files.Launcher/Program.cs

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Collections.Generic;
55
using System.ComponentModel;
66
using System.Diagnostics;
7+
using System.Drawing;
8+
using System.Drawing.Imaging;
79
using System.IO;
810
using System.Linq;
911
using System.Runtime.InteropServices;
@@ -255,6 +257,12 @@ private static async void Connection_RequestReceived(AppServiceConnection sender
255257
responseSet.Add("MUIVerbString", Win32API.ExtractStringFromDLL((string)args.Request.Message["MUIVerbLocation"], (int)args.Request.Message["MUIVerbLine"]));
256258
await args.Request.SendResponseAsync(responseSet);
257259
break;
260+
case "ParseAguments":
261+
var responseArray = new ValueSet();
262+
var resultArgument = Win32API.CommandLineToArgs((string)args.Request.Message["Command"]);
263+
responseArray.Add("ParsedArguments", Newtonsoft.Json.JsonConvert.SerializeObject(resultArgument));
264+
await args.Request.SendResponseAsync(responseArray);
265+
break;
258266
default:
259267
if(args.Request.Message.ContainsKey("Application"))
260268
{

Files.Launcher/Win32API.cs

+34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Drawing;
34
using System.Linq;
45
using System.Runtime.InteropServices;
56
using System.Text;
@@ -81,6 +82,7 @@ public static IList<object> GetFileProperties(ShellItem folderItem, List<(Vanara
8182
[return: MarshalAs(UnmanagedType.Bool)]
8283
private static extern bool FreeLibrary(IntPtr hModule);
8384

85+
8486
public static string ExtractStringFromDLL(string file, int number)
8587
{
8688
IntPtr lib = LoadLibrary(file);
@@ -89,5 +91,37 @@ public static string ExtractStringFromDLL(string file, int number)
8991
FreeLibrary(lib);
9092
return result.ToString();
9193
}
94+
95+
96+
[DllImport("shell32.dll", SetLastError = true)]
97+
public static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
98+
99+
[DllImport("kernel32.dll")]
100+
public static extern IntPtr LocalFree(IntPtr hMem);
101+
102+
public static string[] CommandLineToArgs(string commandLine)
103+
{
104+
if (String.IsNullOrEmpty(commandLine))
105+
return Array.Empty<string>();
106+
107+
var argv = CommandLineToArgvW(commandLine, out int argc);
108+
if (argv == IntPtr.Zero)
109+
throw new System.ComponentModel.Win32Exception();
110+
try
111+
{
112+
var args = new string[argc];
113+
for (var i = 0; i < args.Length; i++)
114+
{
115+
var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
116+
args[i] = Marshal.PtrToStringUni(p);
117+
}
118+
119+
return args;
120+
}
121+
finally
122+
{
123+
Marshal.FreeHGlobal(argv);
124+
}
125+
}
92126
}
93127
}

Files/BaseLayout.cs

+17-23
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
using Windows.UI.Core;
1616
using Windows.UI.Xaml;
1717
using Windows.UI.Xaml.Controls;
18+
using Windows.UI.Xaml.Controls.Primitives;
1819
using Windows.UI.Xaml.Input;
20+
using Windows.UI.Xaml.Media;
21+
using Windows.UI.Xaml.Media.Imaging;
1922
using Windows.UI.Xaml.Navigation;
2023

2124
namespace Files
@@ -118,7 +121,7 @@ private void ClearShellContextMenus()
118121
{
119122
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(contextMenuItems[i]));
120123
}
121-
if (BaseLayoutItemContextFlyout.Items[BaseLayoutItemContextFlyout.Items.Count-1] is MenuFlyoutSeparator flyoutSeperator)
124+
if (BaseLayoutItemContextFlyout.Items[0] is MenuFlyoutSeparator flyoutSeperator)
122125
{
123126
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(flyoutSeperator));
124127
}
@@ -127,24 +130,19 @@ private void ClearShellContextMenus()
127130
public virtual async System.Threading.Tasks.Task SetShellContextmenu()
128131
{
129132
ClearShellContextMenus();
130-
if (_SelectedItems != null)
133+
if (_SelectedItems != null && _SelectedItems.Count > 0)
131134
{
132-
if (!(BaseLayoutItemContextFlyout.Items[BaseLayoutItemContextFlyout.Items.Count - 1] is MenuFlyoutSeparator))
133-
{
134-
BaseLayoutItemContextFlyout.Items.Add(new MenuFlyoutSeparator());
135-
}
136135
var currentBaseLayoutItemCount = BaseLayoutItemContextFlyout.Items.Count;
137136
var isDirectory = !_SelectedItems.Any(c=> c.PrimaryItemAttribute == StorageItemTypes.File || c.PrimaryItemAttribute == StorageItemTypes.None);
138137
foreach (var selectedItem in _SelectedItems)
139138
{
140139
var menuFlyoutItems = await new RegistryReader().GetExtensionContextMenuForFiles(isDirectory, selectedItem.FileExtension);
141140
LoadMenuFlyoutItem(menuFlyoutItems);
142141
}
143-
144-
if(currentBaseLayoutItemCount == BaseLayoutItemContextFlyout.Items.Count &&
145-
BaseLayoutItemContextFlyout.Items[BaseLayoutItemContextFlyout.Items.Count - 1] is MenuFlyoutSeparator flyoutSeperator)
142+
var totalFlyoutItems = BaseLayoutItemContextFlyout.Items.Count - currentBaseLayoutItemCount;
143+
if (totalFlyoutItems > 0 && !(BaseLayoutItemContextFlyout.Items[totalFlyoutItems] is MenuFlyoutSeparator))
146144
{
147-
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(flyoutSeperator));
145+
BaseLayoutItemContextFlyout.Items.Insert(totalFlyoutItems, new MenuFlyoutSeparator());
148146
}
149147
}
150148
}
@@ -239,15 +237,15 @@ private void LoadMenuFlyoutItem(IEnumerable<(string commandKey,string commandNam
239237
{
240238
continue;
241239
}
242-
240+
243241
var menuLayoutItem = new MenuFlyoutItem()
244242
{
245243
Text = menuFlyoutItem.commandName,
246244
Tag = menuFlyoutItem
247245
};
248246
menuLayoutItem.Click += MenuLayoutItem_Click;
249247

250-
BaseLayoutItemContextFlyout.Items.Add(menuLayoutItem);
248+
BaseLayoutItemContextFlyout.Items.Insert(0, menuLayoutItem);
251249
}
252250
}
253251

@@ -271,28 +269,24 @@ private async void MenuLayoutItem_Click(object sender, RoutedEventArgs e)
271269
var (_, _, _, command) = ParseContextMenuTag(currentMenuLayoutItem.Tag);
272270
if (selectedFileSystemItems.Count > 1)
273271
{
274-
var commandsToExecute = new List<string>();
275272
foreach (var selectedDataItem in selectedFileSystemItems)
276273
{
277-
var commandToExecute = command?.Replace("%1", selectedDataItem.ItemPath);
278-
if (!string.IsNullOrEmpty(commandToExecute))
274+
var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
275+
if (!string.IsNullOrEmpty(commandToExecute.command))
279276
{
280-
commandsToExecute.Add(commandToExecute);
277+
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
281278
}
282-
}
283-
if(commandsToExecute.Count > 0)
284-
{
285-
await Interaction.InvokeWin32Components(commandsToExecute);
279+
286280
}
287281
}
288282
else if (selectedFileSystemItems.Count == 1)
289283
{
290284
var selectedDataItem = selectedFileSystemItems[0] as ListedItem;
291285

292-
var commandToExecute = command?.Replace("%1", selectedDataItem.ItemPath);
293-
if (!string.IsNullOrEmpty(commandToExecute))
286+
var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
287+
if (!string.IsNullOrEmpty(commandToExecute.command))
294288
{
295-
await Interaction.InvokeWin32Component(commandToExecute);
289+
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
296290
}
297291
}
298292
}

Files/Files.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
<Compile Include="Helpers\NativeFindStorageItemHelper.cs" />
167167
<Compile Include="Helpers\NaturalStringComparer.cs" />
168168
<Compile Include="Helpers\PackageHelper.cs" />
169+
<Compile Include="Helpers\ShellCommandParser.cs" />
169170
<Compile Include="Helpers\StringExtensions.cs" />
170171
<Compile Include="Helpers\ThemeHelper.cs" />
171172
<Compile Include="Program.cs" />

Files/Helpers/RegistryReader.cs

+44-28
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
using Microsoft.Win32;
22
using System;
33
using System.Collections.Generic;
4+
using System.IO;
45
using System.Linq;
6+
using System.Runtime.InteropServices.WindowsRuntime;
57
using System.Security.Principal;
68
using System.Text;
79
using System.Threading.Tasks;
810
using Windows.Foundation.Collections;
11+
using Windows.Graphics.Imaging;
12+
using Windows.UI.Xaml.Controls;
13+
using Windows.UI.Xaml.Media.Imaging;
914

1015
namespace Files.Helpers
1116
{
@@ -18,45 +23,56 @@ private async Task ParseRegistryAndAddToList(List<(string commandKey, string com
1823
{
1924
foreach (var keyname in shellKey.GetSubKeyNames())
2025
{
21-
var commandNameKey = shellKey.OpenSubKey(keyname);
22-
var commandName = commandNameKey.GetValue(String.Empty)?.ToString() ?? "";
23-
//@ is a special command under the registry. We need to search for MUIVerb:
24-
if (string.IsNullOrEmpty(commandName) || commandName.StartsWith("@"))
26+
try
2527
{
26-
27-
var muiVerb = commandNameKey.GetValue("MUIVerb")?.ToString() ?? "";
28-
if (!string.IsNullOrEmpty(muiVerb) && App.Connection != null)
28+
var commandNameKey = shellKey.OpenSubKey(keyname);
29+
var commandName = commandNameKey.GetValue(String.Empty)?.ToString() ?? "";
30+
//@ is a special command under the registry. We need to search for MUIVerb:
31+
if (string.IsNullOrEmpty(commandName) || commandName.StartsWith("@"))
2932
{
30-
var value = new ValueSet();
31-
value.Add("Arguments", "LoadMUIVerb");
32-
value.Add("MUIVerbLocation", muiVerb?.Split(',')[0]?.TrimStart('@'));
33-
value.Add("MUIVerbLine", Convert.ToInt32(muiVerb?.Split(',')[1]?.TrimStart('-')));
34-
var response = await App.Connection.SendMessageAsync(value);
35-
if (response.Status == Windows.ApplicationModel.AppService.AppServiceResponseStatus.Success
36-
&& response.Message.ContainsKey("MUIVerbString"))
33+
34+
var muiVerb = commandNameKey.GetValue("MUIVerb")?.ToString() ?? "";
35+
if (!string.IsNullOrEmpty(muiVerb) && App.Connection != null)
36+
{
37+
var muiVerbRequest = new ValueSet
3738
{
38-
commandName = (string)response.Message["MUIVerbString"];
39-
if (string.IsNullOrEmpty(commandName))
39+
{ "Arguments", "LoadMUIVerb" },
40+
{ "MUIVerbLocation", muiVerb?.Split(',')[0]?.TrimStart('@') },
41+
{ "MUIVerbLine", Convert.ToInt32(muiVerb?.Split(',')[1]?.TrimStart('-')) }
42+
};
43+
var responseMUIVerb = await App.Connection.SendMessageAsync(muiVerbRequest);
44+
if (responseMUIVerb.Status == Windows.ApplicationModel.AppService.AppServiceResponseStatus.Success
45+
&& responseMUIVerb.Message.ContainsKey("MUIVerbString"))
4046
{
41-
continue;
47+
commandName = (string)responseMUIVerb.Message["MUIVerbString"];
48+
if (string.IsNullOrEmpty(commandName))
49+
{
50+
continue;
51+
}
4252
}
4353
}
54+
else
55+
{
56+
continue;
57+
}
4458
}
45-
else
59+
var commandNameString = commandName.Replace("&", "");
60+
var commandIconString = commandNameKey.GetValue("Icon")?.ToString();
61+
62+
63+
var commandNameKeyNames = commandNameKey.GetSubKeyNames();
64+
if (commandNameKeyNames.Contains("command") && !shellList.Any(c => c.commandKey == keyname))
4665
{
47-
continue;
66+
var command = commandNameKey.OpenSubKey("command");
67+
shellList.Add((commandKey: keyname, commandNameString, commandIconString, command: command.GetValue(string.Empty).ToString()));
68+
4869
}
4970
}
50-
var commandNameString = commandName.Replace("&", "");
51-
var commandIcon = commandNameKey.GetValue("Icon")?.ToString();
52-
53-
var commandNameKeyNames = commandNameKey.GetSubKeyNames();
54-
if (commandNameKeyNames.Contains("command") && !shellList.Any(c => c.commandKey == keyname))
71+
catch
5572
{
56-
var command = commandNameKey.OpenSubKey("command");
57-
shellList.Add((commandKey: keyname, commandNameString, commandIcon, command: command.GetValue(string.Empty).ToString()));
58-
73+
continue;
5974
}
75+
6076
}
6177
}
6278
}
@@ -91,7 +107,7 @@ private async Task ParseRegistryAndAddToList(List<(string commandKey, string com
91107

92108
return shellList;
93109
}
94-
catch(Exception ex)
110+
catch
95111
{
96112
return shellList;
97113
}

Files/Helpers/ShellCommandParser.cs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
using System.Threading.Tasks;
8+
using Windows.Foundation.Collections;
9+
10+
namespace Files.Helpers
11+
{
12+
class ShellCommandParser
13+
{
14+
/*
15+
16+
%0 or %1 – The first file parameter. For example "C:\Users\Eric\Desktop\New Text Document.txt". Generally this should be in quotes and the applications command line parsing should accept quotes to disambiguate files with spaces in the name and different command line parameters (this is a security best practice and I believe mentioned in MSDN).
17+
18+
%<n> (where <n> is 2-9) – Replace with the nth parameter.
19+
20+
%s – Show command.
21+
22+
%h – Hotkey value.
23+
24+
%i – IDList stored in a shared memory handle is passed here.
25+
26+
%l – Long file name form of the first parameter. Note that Win32/64 applications will be passed the long file name, whereas Win16 applications get the short file name. Specifying %l is preferred as it avoids the need to probe for the application type.
27+
28+
%d – Desktop absolute parsing name of the first parameter (for items that don't have file system paths).
29+
30+
%v – For verbs that are none implies all. If there is no parameter passed this is the working directory.
31+
32+
%w – The working directory.
33+
*/
34+
public async Task<(string command, string arguments)> ParseShellCommand(string command, string itemPath)
35+
{
36+
if(string.IsNullOrEmpty(command) || string.IsNullOrEmpty(itemPath))
37+
{
38+
return (null, null);
39+
}
40+
41+
var value = new ValueSet
42+
{
43+
{ "Arguments", "ParseAguments" },
44+
{ "Command", command}
45+
};
46+
var response = await App.Connection.SendMessageAsync(value);
47+
if (response.Status == Windows.ApplicationModel.AppService.AppServiceResponseStatus.Success
48+
&& response.Message.ContainsKey("ParsedArguments"))
49+
{
50+
51+
var commandToExecute = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>((string)response.Message["ParsedArguments"]);
52+
53+
var resultCommand = string.Join(" ", commandToExecute.Skip(1));
54+
var shellFileNameRegex = new Regex("(%[0-9]|%D|%L|%U|%V)", RegexOptions.IgnoreCase);
55+
resultCommand = shellFileNameRegex.Replace(resultCommand, $"\"{itemPath}\"");
56+
57+
var shellParentFolderRegex = new Regex("%W", RegexOptions.IgnoreCase);
58+
var fileInfo = new FileInfo(itemPath);
59+
resultCommand = shellParentFolderRegex.Replace(resultCommand, $"\"{fileInfo.Directory.FullName}\"");
60+
61+
var shellHotKeyValueRegex = new Regex("%H", RegexOptions.IgnoreCase);
62+
resultCommand = shellHotKeyValueRegex.Replace(resultCommand, "0");
63+
64+
var shelShowCommandRegex = new Regex("%S", RegexOptions.IgnoreCase);
65+
resultCommand = shelShowCommandRegex.Replace(resultCommand, "1");
66+
return (commandToExecute[0], resultCommand);
67+
}
68+
else
69+
{
70+
return (null, null);
71+
}
72+
73+
}
74+
}
75+
}

Files/UserControls/LayoutModes/GenericFileBrowser.xaml.cs

+1
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ private async void AllView_SelectionChanged(object sender, SelectionChangedEvent
321321
{
322322
AllView.CommitEdit();
323323
SelectedItems = AllView.SelectedItems.Cast<ListedItem>().ToList();
324+
324325
await SetShellContextmenu();
325326
}
326327

0 commit comments

Comments
 (0)