-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWindex.cs
More file actions
598 lines (525 loc) · 24.8 KB
/
Copy pathWindex.cs
File metadata and controls
598 lines (525 loc) · 24.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
// Copyright ©2026 Gatongone
// Author: Gatongone
// Email: gatongone@gmail.com
// Created On: 2026/01/23-11:09:10
// Github: https://github.com/Gatongone
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
/// <summary>
/// Provides helpers to export and locate Windows drivers using WMI, pnputil and SetupDi APIs.
/// Contains interop definitions and utility methods used by the command-line tool.
/// </summary>
internal static class Windex
{
#region Windows API
/// <summary>
/// Retrieves a device information set that contains requested device information elements for a local computer.
/// </summary>
/// <param name="classGuid">Reference to a device setup class GUID. Pass <see cref="Guid.Empty"/> to include all classes.</param>
/// <param name="enumerator">Optional device instance identifier string or null.</param>
/// <param name="hwndParent">Handle to a top-level window to be used for any user interface that is associated with installing a device instance.</param>
/// <param name="flags">Flags that specify what types of device information elements to include.</param>
/// <returns>Handle to the device information set. Returns <see cref="IntPtr.Zero"/> or <c>new IntPtr(-1)</c> on failure.</returns>
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SetupDiGetClassDevs(
ref Guid classGuid,
string enumerator,
IntPtr hwndParent,
uint flags);
/// <summary>
/// Enumerates the device information elements in a device information set.
/// </summary>
/// <param name="deviceInfoSet">Handle to the device information set returned by <see cref="SetupDiGetClassDevs"/>.</param>
/// <param name="memberIndex">Zero-based index of the member to retrieve.</param>
/// <param name="deviceInfoData">Receives information about the enumerated device.</param>
/// <returns><c>true</c> if the function succeeds; otherwise <c>false</c>.</returns>
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
private static extern bool SetupDiEnumDeviceInfo(
IntPtr deviceInfoSet,
uint memberIndex,
ref SP_DEVINFO_DATA deviceInfoData);
/// <summary>
/// Retrieves the device instance ID for a device information element.
/// </summary>
/// <param name="deviceInfoSet">Handle to the device information set.</param>
/// <param name="deviceInfoData">Device information element.</param>
/// <param name="deviceInstanceId">StringBuilder to receive the device instance ID.</param>
/// <param name="deviceInstanceIdSize">Size of the buffer in characters.</param>
/// <param name="requiredSize">Receives the number of characters required to hold the full device instance ID.</param>
/// <returns><c>true</c> if successful; otherwise <c>false</c>.</returns>
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
private static extern bool SetupDiGetDeviceInstanceId(
IntPtr deviceInfoSet,
ref SP_DEVINFO_DATA deviceInfoData,
StringBuilder deviceInstanceId,
int deviceInstanceIdSize,
out int requiredSize);
/// <summary>
/// Retrieves a specified Plug and Play device property.
/// </summary>
/// <param name="deviceInfoSet">Handle to the device information set.</param>
/// <param name="deviceInfoData">Device information element.</param>
/// <param name="property">Property identifier (for example <c>SPDRP_DRIVER</c>).</param>
/// <param name="propertyRegDataType">Receives the registry data type of the property.</param>
/// <param name="propertyBuffer">Buffer to receive property data.</param>
/// <param name="propertyBufferSize">Size of the buffer in bytes.</param>
/// <param name="requiredSize">Receives the number of bytes required to store the property.</param>
/// <returns><c>true</c> on success; otherwise <c>false</c>.</returns>
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
private static extern bool SetupDiGetDeviceRegistryProperty(
IntPtr deviceInfoSet,
ref SP_DEVINFO_DATA deviceInfoData,
uint property,
out uint propertyRegDataType,
byte[] propertyBuffer,
uint propertyBufferSize,
out uint requiredSize);
/// <summary>
/// Destroys a device information set and frees associated memory.
/// </summary>
/// <param name="deviceInfoSet">Handle to the device information set to destroy.</param>
/// <returns><c>true</c> on success; otherwise <c>false</c>.</returns>
[DllImport("setupapi.dll")]
private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);
/// <summary>
/// Represents the native SP_DEVINFO_DATA structure used by SetupAPI calls.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct SP_DEVINFO_DATA
{
public uint cbSize;
public Guid classGuid;
public uint devInst;
public IntPtr reserved;
}
/// <summary>Flag: Return only devices that are present.</summary>
private const uint DIGCF_PRESENT = 0x00000002;
/// <summary>Flag: Return devices for all classes.</summary>
private const uint DIGCF_ALLCLASSES = 0x00000004;
/// <summary>Property: Driver registry property identifier for SetupAPI.</summary>
private const uint SPDRP_DRIVER = 0x00000009;
/// <summary>Property: Hardware ID registry property identifier for SetupAPI.</summary>
private const uint SPDRP_HARDWAREID = 0x00000001;
/// <summary>Native error: insufficient buffer.</summary>
private const uint ERROR_INSUFFICIENT_BUFFER = 122;
/// <summary>Maximum device id length for buffer allocations.</summary>
private const int MAX_DEVICE_ID_LEN = 200;
#endregion
/// <summary>
/// Simple container for driver metadata used by export operations.
/// </summary>
private class DriverInfo
{
/// <summary>Device ID reported by WMI (DeviceID).</summary>
public string DeviceId { get; set; }
/// <summary>INF filename associated with the driver.</summary>
public string InfName { get; set; }
/// <summary>Driver version string if available.</summary>
public string DriverVersion { get; set; }
/// <summary>Driver date string if available.</summary>
public string DriverDate { get; set; }
}
/// <summary>
/// Exports complete driver files for a specific device by its Device ID.
/// Tries multiple strategies in order: driver store lookup, pnputil export, then SetupDi enumeration.
/// </summary>
/// <param name="deviceId">Device instance ID of the target device.</param>
/// <param name="outputPath">Directory to copy driver files to. Will be created if missing.</param>
/// <param name="exportZip">When true, creates a zip archive from the exported folder and removes the folder.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="deviceId"/> or <paramref name="outputPath"/> are null or empty.</exception>
/// <exception cref="Exception">Thrown when all export methods fail or an unexpected error occurs.</exception>
public static void ExportDriver(string deviceId, string outputPath, bool exportZip = false)
{
if (string.IsNullOrEmpty(deviceId))
throw new ArgumentException("Device ID cannot be null or empty", nameof(deviceId));
if (string.IsNullOrEmpty(outputPath))
throw new ArgumentException("Output path cannot be null or empty", nameof(outputPath));
// Clean up the output path
outputPath = Path.GetFullPath(outputPath);
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}
try
{
if (TryExportFromDriverStore(deviceId, outputPath) || // 1. Try to find driver in driver store using WMI
TryExportWithPnPUtil(deviceId, outputPath) || // 2. Try using pnputil (most reliable)
TryExportWithSetupDi(deviceId, outputPath)) // 3. Try using SetupDi API
{
if (exportZip)
{
ZipFile.CreateFromDirectory(outputPath, $"{outputPath}.zip");
Directory.Delete(outputPath, true);
}
return;
}
throw new Exception("All export methods failed");
}
catch (Exception ex)
{
throw new Exception($"Failed to export driver for device {deviceId}: {ex.Message}", ex);
}
}
/// <summary>
/// Exports complete driver files for all signed drivers discovered on the system.
/// Produces a per-device subfolder (and optional zip) and emits a JSON map file referencing exported archives.
/// </summary>
/// <param name="outputPath">Root directory where driver exports will be stored.</param>
/// <param name="remoteUri">Optional base URI prefix used when writing the repository map.</param>
/// <param name="exportZip">When true, each driver folder will be zipped and the folder removed.</param>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public static void ExportAllDriver(string outputPath, string remoteUri = "", bool exportZip = false)
{
if (string.IsNullOrEmpty(outputPath))
throw new ArgumentException("Output path cannot be null or empty", nameof(outputPath));
outputPath = Path.GetFullPath(outputPath);
Directory.CreateDirectory(outputPath);
try
{
var allDrivers = GetAllDriverInfos().ToArray();
Console.WriteLine($"Found {allDrivers.Length} drivers to export");
var successCount = 0;
var failCount = 0;
var map = new ConcurrentDictionary<string, string>();
if (!string.IsNullOrEmpty(remoteUri) && !remoteUri.EndsWith('/'))
{
remoteUri += '/';
}
Parallel.For(0L, allDrivers.Length, index =>
{
var driver = allDrivers[index];
try
{
var deviceId = driver.DeviceId;
var safeDeviceId = SanitizeFileName(deviceId);
var driverOutputPath = Path.Combine(outputPath, safeDeviceId);
ExportDriver(deviceId, driverOutputPath, exportZip);
map.TryAdd(deviceId, $"{remoteUri}{safeDeviceId}{(exportZip ? ".zip" : "")}");
Interlocked.Increment(ref successCount);
Console.WriteLine($"Exported: {driverOutputPath}{(exportZip ? ".zip" : "")}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to export driver for {driver.DeviceId}: {ex.Message}");
Interlocked.Increment(ref failCount);
}
});
var repoConfigs = Path.Combine(outputPath, "driver_repo.json");
if (File.Exists(repoConfigs))
{
File.Create(repoConfigs).Dispose();
}
else
{
File.Delete(repoConfigs);
File.Create(repoConfigs).Dispose();
}
File.AppendAllText(repoConfigs, JsonSerializer.Serialize(map, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}));
Console.WriteLine($"Remote repo: {repoConfigs}");
Console.WriteLine($"Export completed: {successCount} successful, {failCount} failed");
}
catch (Exception ex)
{
throw new Exception($"Failed to export all drivers: {ex.Message}", ex);
}
}
#region Private Helper Methods
/// <summary>
/// Returns a file-system safe name by replacing invalid filename characters with underscores.
/// </summary>
/// <param name="name">Original file or folder name to sanitize.</param>
/// <returns>Sanitized string safe for use as a filename.</returns>
private static string SanitizeFileName(string name) => string.Join("_", name.Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries));
/// <summary>
/// Attempts to locate and copy driver files from the local Driver Store based on the device's INF name reported by WMI.
/// </summary>
/// <param name="deviceId">Device instance ID to look up.</param>
/// <param name="outputPath">Destination folder to copy driver files into.</param>
/// <returns><c>true</c> if driver files were found and copied; otherwise <c>false</c>.</returns>
private static bool TryExportFromDriverStore(string deviceId, string outputPath)
{
try
{
// Get driver INF name from WMI
var driverInfo = GetDriverInfoFromWmi(deviceId);
if (driverInfo == null || string.IsNullOrEmpty(driverInfo.InfName))
return false;
// Search for driver in Driver Store
var driverStorePath = FindDriverInDriverStore(driverInfo.InfName);
if (!string.IsNullOrEmpty(driverStorePath) && Directory.Exists(driverStorePath))
{
CopyDirectory(driverStorePath, outputPath, true);
return true;
}
return false;
}
catch
{
return false;
}
}
private static bool TryExportWithPnPUtil(string deviceId, string outputPath)
{
try
{
// First, get the INF name
var driverInfo = GetDriverInfoFromWmi(deviceId);
if (driverInfo == null || string.IsNullOrEmpty(driverInfo.InfName))
return false;
var infName = driverInfo.InfName;
// Run pnputil to export the driver
var startInfo = new ProcessStartInfo
{
FileName = "pnputil.exe",
Arguments = $"/export-driver \"{infName}\" \"{outputPath}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
Verb = "runas" // Run as administrator
};
using var process = new Process {StartInfo = startInfo};
process.Start();
process.StandardOutput.ReadToEnd();
process.StandardError.ReadToEnd();
process.WaitForExit(30000); // 30 second timeout
if (process.ExitCode == 0)
{
return true;
}
return false;
}
catch (Exception ex)
{
Console.Error.WriteLine($"PnPUtil method failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Uses pnputil.exe to export driver files by INF name into the specified output folder.
/// </summary>
/// <param name="deviceId">Device instance ID to look up INF name via WMI.</param>
/// <param name="outputPath">Destination folder for exported driver files.</param>
/// <returns><c>true</c> if pnputil successfully exported the driver; otherwise <c>false</c>.</returns>
/// <remarks>
/// This method requires pnputil to be available and will attempt to run it elevated ("runas").
/// It reads process output and returns success only if pnputil returns exit code 0.
/// </remarks>
private static bool TryExportWithSetupDi(string deviceId, string outputPath)
{
var deviceInfoSet = IntPtr.Zero;
try
{
var nullGuid = Guid.Empty;
deviceInfoSet = SetupDiGetClassDevs(ref nullGuid, null, IntPtr.Zero, DIGCF_ALLCLASSES | DIGCF_PRESENT);
if (deviceInfoSet == IntPtr.Zero || deviceInfoSet == new IntPtr(-1))
return false;
var deviceInfoData = new SP_DEVINFO_DATA();
deviceInfoData.cbSize = (uint) Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
uint deviceIndex = 0;
while (SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, ref deviceInfoData))
{
var instanceIdBuilder = new StringBuilder(MAX_DEVICE_ID_LEN);
if (SetupDiGetDeviceInstanceId(deviceInfoSet, ref deviceInfoData, instanceIdBuilder,
instanceIdBuilder.Capacity, out _))
{
var currentDeviceId = instanceIdBuilder.ToString();
if (currentDeviceId.Equals(deviceId, StringComparison.OrdinalIgnoreCase))
{
// Get driver INF path
var propertyBuffer = new byte[1024];
if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, ref deviceInfoData,
SPDRP_DRIVER, out _, propertyBuffer,
(uint) propertyBuffer.Length, out _))
{
var driverPath = Encoding.Unicode.GetString(propertyBuffer).TrimEnd('\0');
if (!string.IsNullOrEmpty(driverPath))
{
// Extract INF name and find it
var infName = Path.GetFileName(driverPath);
var driverStorePath = FindDriverInDriverStore(infName);
if (!string.IsNullOrEmpty(driverStorePath))
{
CopyDirectory(driverStorePath, outputPath, true);
return true;
}
}
}
}
}
deviceIndex++;
}
return false;
}
catch
{
return false;
}
finally
{
if (deviceInfoSet != IntPtr.Zero)
SetupDiDestroyDeviceInfoList(deviceInfoSet);
}
}
/// <summary>
/// Queries Win32_PnPSignedDriver via WMI and returns driver metadata for the specified device ID.
/// </summary>
/// <param name="deviceId">Device instance ID to query.</param>
/// <returns>A <see cref="DriverInfo"/> object when found; otherwise <c>null</c>.</returns>
private static DriverInfo? GetDriverInfoFromWmi(string deviceId)
{
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPSignedDriver");
foreach (var o in searcher.Get())
{
var driver = (ManagementObject) o;
var driverDeviceId = driver["DeviceID"]?.ToString();
if (driverDeviceId != null &&
driverDeviceId.Replace("\\", "").Equals(deviceId.Replace("\\", ""), StringComparison.OrdinalIgnoreCase))
{
return new DriverInfo
{
DeviceId = driverDeviceId,
InfName = driver["InfName"]?.ToString(),
DriverVersion = driver["DriverVersion"]?.ToString(),
DriverDate = driver["DriverDate"]?.ToString()
};
}
}
return null;
}
/// <summary>
/// Retrieves a list of unique driver metadata entries for all signed drivers found via WMI.
/// </summary>
/// <returns>List of <see cref="DriverInfo"/> objects describing discovered drivers.</returns>
private static List<DriverInfo> GetAllDriverInfos()
{
var drivers = new List<DriverInfo>();
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPSignedDriver");
foreach (var driver in searcher.Get())
{
var deviceId = driver["DeviceID"]?.ToString();
var infName = driver["InfName"]?.ToString();
if (string.IsNullOrEmpty(deviceId) || string.IsNullOrEmpty(infName)) continue;
// Check if we already have this INF (avoid duplicates for same driver)
if (drivers.Any(d => d.InfName == infName)) continue;
drivers.Add(new DriverInfo
{
DeviceId = deviceId,
InfName = infName,
DriverVersion = driver["DriverVersion"]?.ToString(),
DriverDate = driver["DriverDate"]?.ToString()
});
}
return drivers;
}
/// <summary>
/// Attempts to locate the folder in the local driver store that contains the given INF file.
/// </summary>
/// <param name="infName">INF file name (for example <c>oem0.inf</c>) or base name.</param>
/// <returns>Local folder path when found; otherwise <c>null</c>.</returns>
private static string? FindDriverInDriverStore(string infName)
{
if (string.IsNullOrEmpty(infName))
return null;
// Remove .inf extension if present for pattern matching
var infBaseName = infName;
if (infBaseName.EndsWith(".inf", StringComparison.OrdinalIgnoreCase))
infBaseName = infBaseName.Substring(0, infBaseName.Length - 4);
// Common Driver Store locations
var driverStorePaths = new[]
{
Path.Combine(Environment.SystemDirectory, "DriverStore", "FileRepository"),
@"C:\Windows\System32\DriverStore\FileRepository",
@"C:\Windows\INF",
Environment.SystemDirectory
};
foreach (var basePath in driverStorePaths)
{
if (!Directory.Exists(basePath))
continue;
try
{
// Search for folders matching the pattern
var directories = Directory.GetDirectories(basePath, "*", SearchOption.AllDirectories);
foreach (var dir in directories)
{
// Check if directory contains the INF file
var infFiles = Directory.GetFiles(dir, "*.inf", SearchOption.TopDirectoryOnly);
foreach (var infFile in infFiles)
{
var fileName = Path.GetFileName(infFile);
if (fileName.Equals(infName, StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith(infBaseName + ".inf", StringComparison.OrdinalIgnoreCase))
{
return dir;
}
}
// Also check directory name pattern
var dirName = Path.GetFileName(dir);
if (dirName.StartsWith(infBaseName + ".inf_", StringComparison.OrdinalIgnoreCase) ||
dirName.Contains(infBaseName))
{
return dir;
}
}
}
catch (UnauthorizedAccessException)
{
// Skip directories we can't access
}
}
return null;
}
/// <summary>
/// Copies the contents of a directory to a target location, optionally recursing into subdirectories.
/// Errors copying individual files are reported to console but do not abort the operation.
/// </summary>
/// <param name="sourceDir">Source directory path.</param>
/// <param name="destDir">Destination directory path. Will be created if missing.</param>
/// <param name="copySubDirs">If <c>true</c> the copy operation will recurse into subdirectories.</param>
/// <exception cref="DirectoryNotFoundException">Thrown when the <paramref name="sourceDir"/> does not exist.</exception>
private static void CopyDirectory(string sourceDir, string destDir, bool copySubDirs)
{
var dir = new DirectoryInfo(sourceDir);
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory does not exist: {dir.FullName}");
var dirs = dir.GetDirectories();
// Create the destination directory if it doesn't exist
Directory.CreateDirectory(destDir);
// Get the files in the directory and copy them
var files = dir.GetFiles();
foreach (var file in files)
{
var tempPath = Path.Combine(destDir, file.Name);
try
{
file.CopyTo(tempPath, true);
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not copy {file.FullName}: {ex.Message}");
}
}
// Copy subdirectories if specified
if (copySubDirs)
{
foreach (var subDir in dirs)
{
var tempPath = Path.Combine(destDir, subDir.Name);
CopyDirectory(subDir.FullName, tempPath, copySubDirs);
}
}
}
#endregion
}