From d29f629cc814b4314691e703561aedd58b4e538c Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:13:55 -0300 Subject: [PATCH 01/12] Keep per-user settings when the portable settings file is invalid Added handling for InvalidDataException in portable settings initialization. --- Rapr/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rapr/Program.cs b/Rapr/Program.cs index 2885c6b..18c4dae 100644 --- a/Rapr/Program.cs +++ b/Rapr/Program.cs @@ -108,7 +108,8 @@ public static void Main() }); } catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException - || ex is IOException || ex is ConfigurationException || ex is System.Xml.XmlException) + || ex is IOException || ex is ConfigurationException || ex is System.Xml.XmlException + || ex is InvalidDataException) { portableSettingsFailed = true; Trace.TraceWarning($"Portable settings could not be initialized; retaining per-user settings: {ex}"); From 87499d6bef40886a72255ac571e5c33d5f30e9fd Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:16:16 -0300 Subject: [PATCH 02/12] Only check culture subfolders when looking for satellite assemblies --- Rapr/DSEFormHelper.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Rapr/DSEFormHelper.cs b/Rapr/DSEFormHelper.cs index b710e80..d89b084 100644 --- a/Rapr/DSEFormHelper.cs +++ b/Rapr/DSEFormHelper.cs @@ -288,25 +288,31 @@ public static ICollection GetSupportedLanguage() } string currentFolder = Path.GetDirectoryName(assembly.Location); + string satelliteFileName = assembly.GetName().Name + ResourceSuffix; try { - DirectoryInfo dir = new DirectoryInfo(currentFolder); - - foreach (var file in dir.EnumerateFiles($"{resourcePrefix}{ResourceSuffix}", SearchOption.AllDirectories)) + // Satellite assemblies live in \\Rapr.resources.dll, so only + // the direct subfolders need to be checked. + foreach (var cultureFolder in new DirectoryInfo(currentFolder).EnumerateDirectories()) { - string folderName = file.Directory.Name; + if (!File.Exists(Path.Combine(cultureFolder.FullName, satelliteFileName))) + { + continue; + } + try { - supportedLanguage.Add(new CultureInfo(folderName)); + supportedLanguage.Add(new CultureInfo(cultureFolder.Name)); } catch (CultureNotFoundException) { } } } - catch (SecurityException) + catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException || ex is IOException) { + Trace.TraceWarning($"Could not scan for satellite resource assemblies: {ex}"); } return supportedLanguage; From 66f682cd561df26d5934d160da5fded39a33917e Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:18:29 -0300 Subject: [PATCH 03/12] Sort non-comparable columns by text when ordering selected drivers --- Rapr/Utils/LinqExtensions.cs | 52 +++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Rapr/Utils/LinqExtensions.cs b/Rapr/Utils/LinqExtensions.cs index 6998eb6..05253d6 100644 --- a/Rapr/Utils/LinqExtensions.cs +++ b/Rapr/Utils/LinqExtensions.cs @@ -1,12 +1,17 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace Rapr.Utils { public static class LinqExtensions { + private static readonly MethodInfo ToSortableStringMethod = + typeof(LinqExtensions).GetMethod(nameof(ToSortableString), BindingFlags.NonPublic | BindingFlags.Static); + public static IOrderedEnumerable OrderByColumnName(this IEnumerable source, string columnName, bool ascending = true) { if (string.IsNullOrEmpty(columnName)) @@ -14,17 +19,13 @@ public static IOrderedEnumerable OrderByColumnName(this IEnumerable sou return source.OrderBy(a => 1); } - var parameter = Expression.Parameter(typeof(T), "x"); - var property = Expression.Property(parameter, columnName); - var propertyType = property.Type; - var lambda = Expression.Lambda(property, parameter); - + var lambda = CreateKeySelector(columnName); var compiledLambda = lambda.Compile(); var orderByMethod = typeof(Enumerable) .GetMethods() .First(m => m.Name == (ascending ? "OrderBy" : "OrderByDescending") && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(T), propertyType); + .MakeGenericMethod(typeof(T), lambda.ReturnType); return (IOrderedEnumerable)orderByMethod.Invoke(null, new object[] { source, compiledLambda }); } @@ -36,20 +37,45 @@ public static IOrderedEnumerable ThenByColumnName(this IOrderedEnumerable< return source; } - var parameter = Expression.Parameter(typeof(T), "x"); - var property = Expression.Property(parameter, columnName); - var propertyType = property.Type; - var lambda = Expression.Lambda(property, parameter); - + var lambda = CreateKeySelector(columnName); var compiledLambda = lambda.Compile(); var thenByMethod = typeof(Enumerable) .GetMethods() .First(m => m.Name == (ascending ? "ThenBy" : "ThenByDescending") && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(T), propertyType); + .MakeGenericMethod(typeof(T), lambda.ReturnType); return (IOrderedEnumerable)thenByMethod.Invoke(null, new object[] { source, compiledLambda }); } + + private static LambdaExpression CreateKeySelector(string columnName) + { + var parameter = Expression.Parameter(typeof(T), "x"); + Expression key = Expression.Property(parameter, columnName); + + // Comparer.Default throws for keys that don't implement IComparable (e.g. List), + // so sort those by their text, the same way ObjectListView does. + if (!IsComparable(key.Type)) + { + key = Expression.Call(ToSortableStringMethod, Expression.Convert(key, typeof(object))); + } + + return Expression.Lambda(key, parameter); + } + + private static bool IsComparable(Type type) + { + return typeof(IComparable).IsAssignableFrom(Nullable.GetUnderlyingType(type) ?? type); + } + + private static string ToSortableString(object value) + { + if (value is string || !(value is IEnumerable items)) + { + return value?.ToString(); + } + + return string.Join(", ", items.Cast()); + } } } - From 3b90191e983a8936582f190e03d8dab3ce4d3d50 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:20:14 -0300 Subject: [PATCH 04/12] Sort non-comparable columns by text when ordering selected drivers --- RaprTests/Utils/LinqExtensionsTests.cs | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 RaprTests/Utils/LinqExtensionsTests.cs diff --git a/RaprTests/Utils/LinqExtensionsTests.cs b/RaprTests/Utils/LinqExtensionsTests.cs new file mode 100644 index 0000000..cd3304a --- /dev/null +++ b/RaprTests/Utils/LinqExtensionsTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Rapr.Utils; + +namespace Rapr.Tests.Utils +{ + [TestClass] + public class LinqExtensionsTests + { + [TestMethod] + public void OrderByColumnWithNonComparableValuesSortsByText() + { + var entries = new List + { + new DriverStoreEntry { DriverPublishedName = "oem2.inf", DriverFiles = new List { "b.sys" } }, + new DriverStoreEntry { DriverPublishedName = "oem3.inf", DriverFiles = null }, + new DriverStoreEntry { DriverPublishedName = "oem1.inf", DriverFiles = new List { "a.sys", "z.dll" } }, + }; + + var ascending = entries.OrderByColumnName(nameof(DriverStoreEntry.DriverFiles)).ToList(); + var descending = entries.OrderByColumnName(nameof(DriverStoreEntry.DriverFiles), ascending: false).ToList(); + + CollectionAssert.AreEqual( + new[] { "oem3.inf", "oem1.inf", "oem2.inf" }, + ascending.Select(e => e.DriverPublishedName).ToArray()); + CollectionAssert.AreEqual( + new[] { "oem2.inf", "oem1.inf", "oem3.inf" }, + descending.Select(e => e.DriverPublishedName).ToArray()); + } + + [TestMethod] + public void ThenByColumnWithNonComparableValuesSortsByText() + { + var entries = new List + { + new DriverStoreEntry { DriverPublishedName = "oem2.inf", DriverClass = "Net", DriverFiles = new List { "b.sys" } }, + new DriverStoreEntry { DriverPublishedName = "oem1.inf", DriverClass = "Net", DriverFiles = new List { "a.sys" } }, + new DriverStoreEntry { DriverPublishedName = "oem3.inf", DriverClass = "Audio", DriverFiles = new List { "c.sys" } }, + }; + + var sorted = entries + .OrderByColumnName(nameof(DriverStoreEntry.DriverClass)) + .ThenByColumnName(nameof(DriverStoreEntry.DriverFiles)) + .ToList(); + + CollectionAssert.AreEqual( + new[] { "oem3.inf", "oem1.inf", "oem2.inf" }, + sorted.Select(e => e.DriverPublishedName).ToArray()); + } + + [TestMethod] + public void OrderByComparableColumnKeepsNativeOrdering() + { + var entries = new List + { + new DriverStoreEntry { DriverPublishedName = "oem1.inf", DriverSize = 100 }, + new DriverStoreEntry { DriverPublishedName = "oem2.inf", DriverSize = 20 }, + }; + + var sorted = entries.OrderByColumnName(nameof(DriverStoreEntry.DriverSize)).ToList(); + + // Numeric, not textual ("100" < "20"), ordering is preserved for IComparable keys. + CollectionAssert.AreEqual( + new[] { "oem2.inf", "oem1.inf" }, + sorted.Select(e => e.DriverPublishedName).ToArray()); + } + } +} From 79919027f4524fdaeaf6a98c7327194d637c4322 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:22:54 -0300 Subject: [PATCH 05/12] exporting the remaining drivers after one export fails Add error handling for driver export failures. --- Rapr/DSEForm.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index 2221e03..234af33 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -1498,7 +1498,18 @@ private async Task ExportDrivers(IEnumerable entries, string destinationPath) { foreach (DriverStoreEntry entry in driverStoreEntries) { - bool succeeded = this.driverStore.ExportDriver(entry, destinationPath); + bool succeeded; + try + { + succeeded = this.driverStore.ExportDriver(entry, destinationPath); + } + catch (Exception ex) when (ex is Win32Exception || ex is IOException || ex is UnauthorizedAccessException) + { + // Keep exporting the remaining packages; report this one as failed. + Trace.TraceError($"Failed to export driver package '{entry.DriverPublishedName}': {ex}"); + succeeded = false; + } + string resultTxt = string.Format( succeeded ? Language.Message_Export_Success : Language.Message_Export_Fail, entry.DriverPublishedName, From acc49d49ef1dd8ae163c5bef919d656eaf319b82 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:48:10 -0300 Subject: [PATCH 06/12] Fix crash on Shift+click of a column header after switching language --- Rapr/DSEForm.cs | 11 +++++++++++ Rapr/MyObjectListView.cs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index 8af08f2..030d3f8 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -1020,6 +1020,10 @@ private void SwitchCulture(object sender, EventArgs e) { byte[] driverStoreViewState = this.lstDriverStoreEntries.SaveState(); + // SaveState/RestoreState only keep the primary sort column, so carry the secondary one over by hand. + int secondarySortColumnIndex = this.lstDriverStoreEntries.AllColumns.IndexOf(this.lstDriverStoreEntries.SecondarySortColumn); + SortOrder secondarySortOrder = this.lstDriverStoreEntries.SecondarySortOrder; + Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = ci; @@ -1034,6 +1038,13 @@ private void SwitchCulture(object sender, EventArgs e) this.RightToLeftLayout = ci.TextInfo.IsRightToLeft; this.SetupListViewColumns(); this.lstDriverStoreEntries.RestoreState(driverStoreViewState); + + if (secondarySortColumnIndex >= 0) + { + this.lstDriverStoreEntries.SecondarySortColumn = this.lstDriverStoreEntries.AllColumns[secondarySortColumnIndex]; + this.lstDriverStoreEntries.SecondarySortOrder = secondarySortOrder; + } + this.lstDriverStoreEntries.RightToLeft = this.RightToLeft; this.lstDriverStoreEntries.RightToLeftLayout = this.RightToLeftLayout; this.UpdateDriverStore(this.driverStore); diff --git a/Rapr/MyObjectListView.cs b/Rapr/MyObjectListView.cs index 452c2bf..2986415 100644 --- a/Rapr/MyObjectListView.cs +++ b/Rapr/MyObjectListView.cs @@ -28,7 +28,7 @@ protected override void HandleColumnClick(object sender, ColumnClickEventArgs e) newPrimarySortColumn = this.PrimarySortColumn.Index; // Update secondary column when shift key is down. - if (e.Column == this.SecondarySortColumn.Index) + if (this.SecondarySortColumn != null && e.Column == this.SecondarySortColumn.Index) { // Toggle the secondary column sorting direction. this.SecondarySortOrder = (this.SecondarySortOrder == SortOrder.Descending From ff04f264af77a02f49bb89ae3a94cc363654d5f4 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:48:52 -0300 Subject: [PATCH 07/12] Don't reopen the window minimized --- Rapr/DSEForm.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index 030d3f8..9dbc99f 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -1142,8 +1142,17 @@ private void DSEForm_FormClosing(object sender, FormClosingEventArgs e) { Settings.Default.DriverStoreViewState = Convert.ToBase64String(this.lstDriverStoreEntries.SaveState()); - Settings.Default.WindowState = this.WindowState; - Settings.Default.WindowLocation = this.Location; + if (this.WindowState == FormWindowState.Minimized) + { + // Don't reopen minimized, and don't save the off-screen (-32000, -32000) position. + Settings.Default.WindowState = FormWindowState.Normal; + Settings.Default.WindowLocation = this.RestoreBounds.Location; + } + else + { + Settings.Default.WindowState = this.WindowState; + Settings.Default.WindowLocation = this.Location; + } if (this.WindowState == FormWindowState.Normal) { From bc9f3ab37c99279d31960312b8dd3f363bb43a11 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:49:32 -0300 Subject: [PATCH 08/12] Disable "Open folder" and "Copy folder path" when the folder is unknown --- Rapr/DSEForm.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index 9dbc99f..ee171f7 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -766,8 +766,12 @@ private void ContextMenuStrip_Opening(object sender, CancelEventArgs e) this.ctxMenuSelect.Enabled = true; this.ctxMenuOpenDeviceProperties.Enabled = !string.IsNullOrEmpty((this.lstDriverStoreEntries.SelectedObject as DriverStoreEntry)?.DeviceId); - this.ctxMenuOpenFolder.Enabled = this.lstDriverStoreEntries.SelectedObjects.Count == 1; - this.ctxMenuCopyFolderPath.Enabled = this.lstDriverStoreEntries.SelectedObjects.Count == 1; + + // The folder is unknown when the PnpUtil backend can't match the package to the driver store. + bool hasFolder = this.lstDriverStoreEntries.SelectedObjects.Count == 1 + && !string.IsNullOrEmpty((this.lstDriverStoreEntries.SelectedObject as DriverStoreEntry)?.DriverFolderLocation); + this.ctxMenuOpenFolder.Enabled = hasFolder; + this.ctxMenuCopyFolderPath.Enabled = hasFolder; } else { @@ -1241,6 +1245,11 @@ private void CtxMenuOpenFolder_Click(object sender, EventArgs e) if (this.lstDriverStoreEntries.SelectedObject != null) { DriverStoreEntry item = (DriverStoreEntry)this.lstDriverStoreEntries.SelectedObject; + if (string.IsNullOrEmpty(item.DriverFolderLocation)) + { + return; + } + Process.Start("explorer.exe", "/select, " + Path.Combine(item.DriverFolderLocation, item.DriverInfName)); } } @@ -1250,7 +1259,12 @@ private void CtxMenuCopyFolderPath_Click(object sender, EventArgs e) if (this.lstDriverStoreEntries.SelectedObject != null) { DriverStoreEntry item = (DriverStoreEntry)this.lstDriverStoreEntries.SelectedObject; - Clipboard.SetText(item.DriverFolderLocation); + + // Clipboard.SetText throws ArgumentNullException for an empty string. + if (!string.IsNullOrEmpty(item.DriverFolderLocation)) + { + Clipboard.SetText(item.DriverFolderLocation); + } } } From e807cd956f5cfd86466ad0cbec270ed6f8ff8f81 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:50:12 -0300 Subject: [PATCH 09/12] Show an error instead of crashing when the INF search fails --- Rapr/DSEForm.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index ee171f7..03bb133 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -611,7 +611,17 @@ private async void ButtonAddDriver_Click(object sender, EventArgs e) string infPath = dialog.FileName; bool installDriver = this.cbAddInstall.Checked; - var infFiles = FindInfFile(infPath).ToList(); + List infFiles; + try + { + infFiles = FindInfFile(infPath).ToList(); + } + catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException) + { + // e.g. a subfolder we can't read, or a path that is too long in a deep driver bundle. + this.ShowStatus(Status.Error, ex.Message, ex.ToString(), true); + return; + } if (infFiles.Count == 0) { From 32a319d5a4c27673e0fa03f93f283be7c486ea58 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:50:52 -0300 Subject: [PATCH 10/12] Add every INF in the selected folder, not just the first one --- Rapr/DSEForm.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index 03bb133..edc06a6 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -1699,11 +1699,15 @@ private void UpdateSearchFilter() private IEnumerable FindInfFile(string infPath) { - // Search for *.inf files in the current directory + // Search for *.inf files in the current directory. A package folder can hold several INFs, so add them all. var infFiles = Directory.GetFiles(infPath, "*.inf", SearchOption.TopDirectoryOnly); if (infFiles.Length > 0) { - yield return infFiles[0]; + foreach (var infFile in infFiles) + { + yield return infFile; + } + yield break; } From 075e6b26378ee517b42e2cc88ec103e26d8942a6 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:51:25 -0300 Subject: [PATCH 11/12] Handle access denied when exporting the driver list to CSV --- Rapr/DSEForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rapr/DSEForm.cs b/Rapr/DSEForm.cs index edc06a6..fd82fd7 100644 --- a/Rapr/DSEForm.cs +++ b/Rapr/DSEForm.cs @@ -1017,7 +1017,7 @@ private void ExportDriverList(IEnumerable objects) this.ShowStatus(Status.Normal, message, usePopup: true); } } - catch (Exception ex) when (ex is IOException || ex is SecurityException) + catch (Exception ex) when (ex is IOException || ex is SecurityException || ex is UnauthorizedAccessException) { this.ShowStatus(Status.Error, string.Format(Language.Export_Failed, ex), usePopup: true); } From 00095224c26c5f7a1cd7a344b5a61f3a324d36d3 Mon Sep 17 00:00:00 2001 From: indep-arg <333053532+indep-arg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:52:09 -0300 Subject: [PATCH 12/12] Don't fail the whole DISM listing when one driver folder can't be read --- Rapr/Utils/DismUtil.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Rapr/Utils/DismUtil.cs b/Rapr/Utils/DismUtil.cs index accb58b..dbf3a2c 100644 --- a/Rapr/Utils/DismUtil.cs +++ b/Rapr/Utils/DismUtil.cs @@ -94,7 +94,7 @@ public List EnumeratePackages() DriverDate = driverPackage.Date, DriverVersion = driverPackage.Version, DriverFolderLocation = Path.GetDirectoryName(driverPackage.OriginalFileName), - DriverSize = DriverStoreRepository.GetFolderSize(new DirectoryInfo(Path.GetDirectoryName(driverPackage.OriginalFileName))), + DriverSize = GetDriverFolderSize(Path.GetDirectoryName(driverPackage.OriginalFileName)), BootCritical = driverPackage.BootCritical, }; @@ -112,6 +112,20 @@ public List EnumeratePackages() : driverStoreEntries; } + private static long GetDriverFolderSize(string driverFolderLocation) + { + // One missing or unreadable folder shouldn't make the whole list fail; NativeDriverStore does the same. + try + { + return DriverStoreRepository.GetFolderSize(new DirectoryInfo(driverFolderLocation)); + } + catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) + { + Trace.TraceWarning($"Failed to get folder size for '{driverFolderLocation}': {ex}"); + return 0; + } + } + private DismSession GetSession() { switch (this.Type)