1275 lines
40 KiB
C
1275 lines
40 KiB
C
#include "precomp.h"
|
|
#pragma hdrstop
|
|
#include <initguid.h>
|
|
|
|
//
|
|
// Define and initialize a global variable, GUID_NULL
|
|
// (from coguid.h)
|
|
//
|
|
DEFINE_GUID(GUID_NULL, 0L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
|
|
//
|
|
// Define the size (in characters) of a GUID string, including terminating NULL.
|
|
//
|
|
#define GUID_STRING_LEN (39)
|
|
|
|
//
|
|
// Define a global variable to hold the source path to be used.
|
|
//
|
|
TCHAR SourcePath[MAX_PATH];
|
|
|
|
//
|
|
// Define a file enumeration callback prototype.
|
|
// (Used by pSysSetupEnumerateFiles)
|
|
//
|
|
typedef BOOL (*PFILEENUM_CALLBACK) (
|
|
IN PCTSTR,
|
|
IN OUT PVOID
|
|
);
|
|
|
|
//
|
|
// Private function prototypes:
|
|
//
|
|
BOOL
|
|
PrecompileSingleInf(
|
|
IN PCTSTR FullInfPath,
|
|
IN OUT PVOID Context
|
|
);
|
|
|
|
BOOL
|
|
DeleteSinglePnf(
|
|
IN PCTSTR FullPnfPath,
|
|
IN OUT PVOID Context
|
|
);
|
|
|
|
VOID
|
|
pSysSetupEnumerateFiles(
|
|
IN OUT PWSTR SearchSpec,
|
|
IN PFILEENUM_CALLBACK FileEnumCallback,
|
|
IN OUT PVOID Context
|
|
);
|
|
|
|
#if 1
|
|
UINT
|
|
FileQueueScanCallback(
|
|
IN PVOID Context,
|
|
IN UINT Notification,
|
|
IN UINT_PTR Param1,
|
|
IN UINT_PTR Param2
|
|
);
|
|
#else
|
|
UINT
|
|
DriverListMsgHandler(
|
|
IN PVOID Context,
|
|
IN UINT Notification,
|
|
IN UINT_PTR Param1,
|
|
IN UINT_PTR Param2
|
|
);
|
|
#endif
|
|
|
|
DWORD
|
|
FilesFromInfSectionAndNeededSections(
|
|
IN HINF InfHandle,
|
|
IN PCTSTR InfPath,
|
|
IN PCTSTR SectionName,
|
|
IN HDEVINFO DeviceInfoSet, OPTIONAL
|
|
IN PSP_DEVINFO_DATA DeviceInfoData, OPTIONAL
|
|
IN HSPFILEQ UserFileQ OPTIONAL
|
|
);
|
|
|
|
BOOL
|
|
GetActualSectionToInstallEx(
|
|
IN HINF InfHandle,
|
|
IN PCTSTR InfSectionName,
|
|
IN WORD ProcessorArchitecture,
|
|
OUT PTSTR InfSectionWithExt,
|
|
IN DWORD InfSectionWithExtSize
|
|
);
|
|
|
|
VOID
|
|
Usage(
|
|
VOID
|
|
);
|
|
|
|
int
|
|
__cdecl
|
|
main(
|
|
IN int argc,
|
|
IN char *argv[]
|
|
)
|
|
{
|
|
int StringLen;
|
|
HDEVINFO DeviceInfoSet;
|
|
SP_DEVINFO_DATA DeviceInfoData;
|
|
DWORD Err = NO_ERROR;
|
|
DWORD i;
|
|
SP_DRVINFO_DATA DriverInfoData;
|
|
HSPFILEQ QueueHandle = INVALID_HANDLE_VALUE;
|
|
SP_DEVINSTALL_PARAMS DeviceInstallParams;
|
|
DWORD ScanResult;
|
|
// SP_DRVINSTALL_PARAMS DriverInstallParams;
|
|
TCHAR SearchSpec[MAX_PATH];
|
|
BOOL CacheEnable;
|
|
BOOL DontCallClassInstaller = FALSE;
|
|
BOOL NonNativeSearch = FALSE;
|
|
BOOL IncludeInfsInList = FALSE;
|
|
BOOL DownLevel = FALSE;
|
|
TCHAR CertClassInfPath[MAX_PATH];
|
|
SP_DRVINFO_DETAIL_DATA DriverInfoDetailData;
|
|
TCHAR TargetInfDirectory[MAX_PATH];
|
|
TCHAR ActualSectionName[LINE_LEN];
|
|
TCHAR ActualSectionName2[LINE_LEN];
|
|
PTSTR p;
|
|
LONG GuidIndex, ClassGuidCount = 0;
|
|
HINF hInf;
|
|
LPGUID ClassGuidList = NULL;
|
|
INFCONTEXT InfContext;
|
|
TCHAR GuidStringBuffer[GUID_STRING_LEN];
|
|
HRESULT hr;
|
|
SP_ALTPLATFORM_INFO_V1 AltPlatformInfo;
|
|
ULONG Ver = 0;
|
|
|
|
ZeroMemory(&AltPlatformInfo,sizeof(AltPlatformInfo));
|
|
AltPlatformInfo.cbSize = sizeof(AltPlatformInfo);
|
|
//
|
|
// Process any optional arguments that may precede the (non-optional)
|
|
// SourcePath.
|
|
//
|
|
*CertClassInfPath = TEXT('\0');
|
|
|
|
for(i = 1; i < (DWORD)argc; i++) {
|
|
|
|
if((*(argv[i]) != TEXT('/')) && (*(argv[i]) != TEXT('-'))) {
|
|
break;
|
|
}
|
|
|
|
if(!(*(argv[i]+1)) || *(argv[i]+2)) {
|
|
Usage();
|
|
return -1;
|
|
}
|
|
|
|
switch(*(argv[i]+1)) {
|
|
|
|
case 'H' :
|
|
case 'h' :
|
|
case '?' :
|
|
//
|
|
// Display usage help
|
|
//
|
|
Usage();
|
|
return -1;
|
|
|
|
case 'S' :
|
|
case 's' :
|
|
//
|
|
// Skip class-/co-installers, queue files directly from INF
|
|
//
|
|
DontCallClassInstaller = TRUE;
|
|
break;
|
|
|
|
case 'F' :
|
|
case 'f' :
|
|
//
|
|
// Filter the list based on the INF in the next argument
|
|
//
|
|
i++;
|
|
|
|
if(i == (DWORD)argc) {
|
|
Usage();
|
|
return -1;
|
|
}
|
|
|
|
#ifdef UNICODE
|
|
|
|
StringLen = MultiByteToWideChar(CP_ACP,
|
|
MB_PRECOMPOSED,
|
|
argv[i],
|
|
-1,
|
|
CertClassInfPath,
|
|
sizeof(CertClassInfPath) / sizeof(WCHAR)
|
|
);
|
|
if(!StringLen) {
|
|
_tprintf(TEXT("CertClassInfPath must be less than %d characters.\n"), MAX_PATH);
|
|
return -1;
|
|
}
|
|
|
|
#else // !UNICODE
|
|
|
|
StringLen = lstrlen(argv[i]);
|
|
if(StringLen >= MAX_PATH) {
|
|
_tprintf(TEXT("CertClassInfPath must be less than %d characters.\n"), MAX_PATH);
|
|
return -1;
|
|
}
|
|
CopyMemory(CertClassInfPath, argv[i], StringLen);
|
|
|
|
#endif // !UNICODE
|
|
|
|
//
|
|
// Now open this INF and retrieve all the GUIDs contained in the
|
|
// [DriverSigningClasses] section.
|
|
//
|
|
hInf = SetupOpenInfFile(CertClassInfPath,
|
|
NULL,
|
|
INF_STYLE_WIN4,
|
|
NULL
|
|
);
|
|
|
|
if(hInf == INVALID_HANDLE_VALUE) {
|
|
_tprintf(TEXT("SetupOpenInfFile failed with %lx\n"), GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
ClassGuidCount = SetupGetLineCount(hInf, TEXT("DriverSigningClasses"));
|
|
|
|
if(ClassGuidCount < 0) {
|
|
_tprintf(TEXT("CertClassInf specified doesn't contain a [DriverSigningClasses] section\n"));
|
|
return -1;
|
|
}
|
|
|
|
if(!ClassGuidCount) {
|
|
//
|
|
// No classes in the section--nothing to do.
|
|
//
|
|
return 0;
|
|
}
|
|
|
|
ClassGuidList = malloc(ClassGuidCount * sizeof(GUID));
|
|
|
|
if(!ClassGuidList) {
|
|
_tprintf(TEXT("Out of memory\n"));
|
|
return -1;
|
|
}
|
|
|
|
for(GuidIndex = 0; GuidIndex < ClassGuidCount; GuidIndex++) {
|
|
if(!SetupGetLineByIndex(hInf,
|
|
TEXT("DriverSigningClasses"),
|
|
GuidIndex,
|
|
&InfContext)) {
|
|
_tprintf(TEXT("SetupGetLineByIndex failed with %lx\n"), GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
if(!SetupGetStringField(&InfContext,
|
|
0,
|
|
GuidStringBuffer,
|
|
sizeof(GuidStringBuffer) / sizeof(TCHAR),
|
|
NULL)) {
|
|
|
|
_tprintf(TEXT("SetupGetStringField failed with %lx\n"), GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
hr = CLSIDFromString(GuidStringBuffer, &(ClassGuidList[GuidIndex]));
|
|
|
|
if(FAILED(hr)) {
|
|
_tprintf(TEXT("CLSIDFromString failed with %lx\n"), hr);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
//
|
|
// We're finished with the INF.
|
|
//
|
|
SetupCloseInfFile(hInf);
|
|
break;
|
|
|
|
case 'A' :
|
|
case 'a' :
|
|
//
|
|
// Target Architecture
|
|
// 0 = Intel x86
|
|
// 6 = IA64
|
|
// 9 = AMD64
|
|
//
|
|
i++;
|
|
|
|
if(i == (DWORD)argc) {
|
|
Usage();
|
|
return -1;
|
|
}
|
|
AltPlatformInfo.ProcessorArchitecture = (WORD)strtoul(argv[i],NULL,0);
|
|
NonNativeSearch = TRUE;
|
|
|
|
break;
|
|
|
|
case 'V' :
|
|
case 'v' :
|
|
//
|
|
// Version, eg 5.1
|
|
//
|
|
i++;
|
|
|
|
if(i == (DWORD)argc) {
|
|
Usage();
|
|
return -1;
|
|
}
|
|
Ver = strtoul(argv[i],NULL,16);
|
|
AltPlatformInfo.MajorVersion = HIBYTE(Ver);
|
|
AltPlatformInfo.MinorVersion = LOBYTE(Ver);
|
|
NonNativeSearch = TRUE;
|
|
break;
|
|
|
|
case 'I' :
|
|
case 'i' :
|
|
//
|
|
// Include INFs in the list
|
|
//
|
|
IncludeInfsInList = TRUE;
|
|
|
|
//
|
|
// Retrieve the location of the INF directory, so when we add
|
|
// the INFs to the copy queue it reports the proper path for
|
|
// their destination.
|
|
//
|
|
StringLen = GetSystemWindowsDirectory(
|
|
TargetInfDirectory,
|
|
sizeof(TargetInfDirectory) / sizeof(TCHAR)
|
|
);
|
|
|
|
if(!StringLen) {
|
|
_tprintf(TEXT("GetSystemWindowsDirectory failed with %lx\n"), GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
if(StringLen >= (sizeof(TargetInfDirectory) / sizeof(TCHAR))) {
|
|
_tprintf(TEXT("GetSystemWindowsDirectory requires a larger buffer than we supplied\n"));
|
|
return -1;
|
|
}
|
|
|
|
if(TargetInfDirectory[StringLen - 1] != TEXT('\\')) {
|
|
TargetInfDirectory[StringLen++] = TEXT('\\');
|
|
}
|
|
lstrcpyn(TargetInfDirectory + StringLen,
|
|
TEXT("INF"),
|
|
(sizeof(TargetInfDirectory) / sizeof(TCHAR)) - StringLen
|
|
);
|
|
break;
|
|
|
|
default :
|
|
//
|
|
// Invalid option
|
|
//
|
|
Usage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
//
|
|
// We should have processed all arguments except the last one, which should
|
|
// contain the SourcePath.
|
|
//
|
|
if(i != ((DWORD)argc - 1)) {
|
|
Usage();
|
|
return -1;
|
|
}
|
|
|
|
#ifdef UNICODE
|
|
|
|
StringLen = MultiByteToWideChar(CP_ACP,
|
|
MB_PRECOMPOSED,
|
|
argv[i],
|
|
-1,
|
|
SourcePath,
|
|
sizeof(SourcePath) / sizeof(WCHAR)
|
|
);
|
|
if(!StringLen) {
|
|
_tprintf(TEXT("SourcePath must be less than %d characters.\n"), MAX_PATH);
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// We don't want StringLen to include the terminating null character.
|
|
//
|
|
StringLen--;
|
|
|
|
#else // !UNICODE
|
|
|
|
StringLen = lstrlen(argv[i]);
|
|
if(!StringLen) {
|
|
_tprintf(TEXT("SourcePath cannot be an empty string.\n"));
|
|
return -1;
|
|
}
|
|
if(StringLen >= MAX_PATH) {
|
|
_tprintf(TEXT("SourcePath must be less than %d characters.\n"), MAX_PATH);
|
|
return -1;
|
|
}
|
|
CopyMemory(SourcePath, argv[i], StringLen);
|
|
|
|
#endif // !UNICODE
|
|
|
|
if(NonNativeSearch) {
|
|
//
|
|
// fix up platform information
|
|
//
|
|
AltPlatformInfo.Platform = VER_PLATFORM_WIN32_NT;
|
|
if(!Ver) {
|
|
AltPlatformInfo.MajorVersion = HIBYTE(_WIN32_WINNT);
|
|
AltPlatformInfo.MinorVersion = LOBYTE(_WIN32_WINNT);
|
|
}
|
|
AltPlatformInfo.Reserved = 0;
|
|
}
|
|
//
|
|
// Precompile all INFs in the specified directory (we'll delete the PNFs
|
|
// when we're done).
|
|
//
|
|
CopyMemory(SearchSpec, SourcePath, StringLen * sizeof(TCHAR));
|
|
if(SearchSpec[StringLen - 1] != TEXT('\\')) {
|
|
SearchSpec[StringLen++] = TEXT('\\');
|
|
}
|
|
|
|
lstrcpyn(SearchSpec + StringLen,
|
|
TEXT("*.INF"),
|
|
(sizeof(SearchSpec) / sizeof(TCHAR)) - StringLen
|
|
);
|
|
|
|
CacheEnable = TRUE;
|
|
pSysSetupEnumerateFiles(SearchSpec, PrecompileSingleInf, &CacheEnable);
|
|
|
|
//
|
|
// Create an HDEVINFO set to contain our placeholder device.
|
|
//
|
|
DeviceInfoSet = SetupDiCreateDeviceInfoList(NULL, NULL);
|
|
if(DeviceInfoSet == INVALID_HANDLE_VALUE) {
|
|
_tprintf(TEXT("SetupDiCreateDeviceInfoList failed with %lx\n"), GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// Now create the placeholder device for which each driver node will be selected/analyzed.
|
|
//
|
|
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
if(!SetupDiCreateDeviceInfo(DeviceInfoSet,
|
|
TEXT("DRIVER_LIST_PLACEHOLDER"),
|
|
(LPGUID)&GUID_NULL,
|
|
NULL,
|
|
NULL,
|
|
DICD_GENERATE_ID,
|
|
&DeviceInfoData)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiCreateDeviceInfo failed with %lx\n"), Err);
|
|
goto clean0;
|
|
}
|
|
|
|
//
|
|
// Create file queue here before search so that we can associate a different
|
|
// platform with the search
|
|
//
|
|
QueueHandle = SetupOpenFileQueue();
|
|
|
|
if(QueueHandle == INVALID_HANDLE_VALUE) {
|
|
//
|
|
// This can only happen due to out-of-memory, and GetLastError() actually isn't even
|
|
// being set currently in this case.
|
|
//
|
|
Err = ERROR_NOT_ENOUGH_MEMORY;
|
|
_tprintf(TEXT("SetupOpenFileQueue failed with %lx\n"), Err);
|
|
goto clean0;
|
|
}
|
|
|
|
if(NonNativeSearch) {
|
|
if(!SetupSetFileQueueAlternatePlatform(QueueHandle,(PSP_ALTPLATFORM_INFO)&AltPlatformInfo,NULL)) {
|
|
DownLevel = TRUE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Redirect our driver search to look in the specified directory.
|
|
// specify file queue that we'll use to base search on
|
|
// and use for all cumulative installs
|
|
//
|
|
DeviceInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
|
|
if(!SetupDiGetDeviceInstallParams(DeviceInfoSet, &DeviceInfoData, &DeviceInstallParams)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiGetDeviceInstallParams (1st time) failed with %lx\n"), Err);
|
|
goto clean0;
|
|
}
|
|
|
|
//
|
|
// specify search path
|
|
//
|
|
lstrcpy(DeviceInstallParams.DriverPath, SourcePath);
|
|
DeviceInstallParams.FlagsEx |= DI_FLAGSEX_ALLOWEXCLUDEDDRVS;
|
|
//
|
|
// specify file queue to search
|
|
//
|
|
DeviceInstallParams.FileQueue = QueueHandle;
|
|
DeviceInstallParams.Flags |= DI_NOVCP;
|
|
|
|
if(!SetupDiSetDeviceInstallParams(DeviceInfoSet, &DeviceInfoData, &DeviceInstallParams)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiSetDeviceInstallParams (1st time) failed with %lx\n"), Err);
|
|
goto clean0;
|
|
}
|
|
//
|
|
// other flags we desire to have
|
|
//
|
|
if(NonNativeSearch && !DownLevel) {
|
|
//
|
|
// if we can, try to make use of DI_FLAGSEX_ALTPLATFORM_DRVSEARCH
|
|
// this supports manufacturer decoration
|
|
//
|
|
DeviceInstallParams.FlagsEx |= DI_FLAGSEX_ALTPLATFORM_DRVSEARCH;
|
|
if(!SetupDiSetDeviceInstallParams(DeviceInfoSet, &DeviceInfoData, &DeviceInstallParams)) {
|
|
DeviceInstallParams.FlagsEx &= ~DI_FLAGSEX_ALTPLATFORM_DRVSEARCH;
|
|
DownLevel = TRUE;
|
|
}
|
|
}
|
|
//
|
|
// if we can, try to make use of DI_FLAGSEX_NO_CLASSLIST_NODE_MERGE
|
|
// this ensures we look at duplicate nodes (which is broken in itself)
|
|
//
|
|
DeviceInstallParams.FlagsEx |= DI_FLAGSEX_NO_CLASSLIST_NODE_MERGE;
|
|
if(!SetupDiSetDeviceInstallParams(DeviceInfoSet, &DeviceInfoData, &DeviceInstallParams)) {
|
|
DeviceInstallParams.FlagsEx &= ~DI_FLAGSEX_NO_CLASSLIST_NODE_MERGE;
|
|
}
|
|
|
|
//
|
|
// Now build a class driver list with every driver node we support.
|
|
//
|
|
if(!SetupDiBuildDriverInfoList(DeviceInfoSet, &DeviceInfoData, SPDIT_CLASSDRIVER)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiBuildDriverInfoList failed with %lx\n"), Err);
|
|
goto clean0;
|
|
}
|
|
|
|
DriverInfoData.cbSize = sizeof(SP_DRVINFO_DATA);
|
|
for(i = 0;
|
|
SetupDiEnumDriverInfo(DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
SPDIT_CLASSDRIVER,
|
|
i,
|
|
&DriverInfoData);
|
|
i++) {
|
|
|
|
if(!SetupDiSetSelectedDriver(DeviceInfoSet, &DeviceInfoData, &DriverInfoData)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiSetSelectedDriver failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
|
|
//
|
|
// The device information element's class GUID will have automatically
|
|
// been updated by SetupDiSetSelectedDriver to reflect the class of the
|
|
// driver node's INF. If we're supposed to filter based on a certclas.inf,
|
|
// then check to make sure that this class is in our list.
|
|
//
|
|
if(ClassGuidCount) {
|
|
|
|
for(GuidIndex = 0; GuidIndex < ClassGuidCount; GuidIndex++) {
|
|
|
|
if(IsEqualGUID(&(DeviceInfoData.ClassGuid), &(ClassGuidList[GuidIndex]))) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(GuidIndex == ClassGuidCount) {
|
|
//
|
|
// Then we didn't find the class GUID in our list--skip this
|
|
// driver node.
|
|
//
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//
|
|
// If we're also supposed to include the INFs in our list of driver
|
|
// files, then retrieve the detailed driver info to find out what INF
|
|
// is associated with this driver node.
|
|
//
|
|
if(IncludeInfsInList) {
|
|
|
|
DriverInfoDetailData.cbSize = sizeof(SP_DRVINFO_DETAIL_DATA);
|
|
|
|
if(!SetupDiGetDriverInfoDetail(DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
&DriverInfoData,
|
|
&DriverInfoDetailData,
|
|
sizeof(DriverInfoDetailData),
|
|
NULL)) {
|
|
//
|
|
// If we failed with ERROR_INSUFFICIENT_BUFFER, then that's OK.
|
|
//
|
|
Err = GetLastError();
|
|
if(Err != ERROR_INSUFFICIENT_BUFFER) {
|
|
_tprintf(TEXT("SetupDiGetDriverInfoDetail failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
}
|
|
|
|
//
|
|
// OK, now that we have the INF name, add it to our queue.
|
|
//
|
|
p = _tcsrchr(DriverInfoDetailData.InfFileName, TEXT('\\'));
|
|
if(!p || !(*(++p))) {
|
|
_tprintf(TEXT("Invalid INF name encountered\n"));
|
|
goto clean1;
|
|
}
|
|
|
|
if(!SetupQueueCopy(QueueHandle,
|
|
SourcePath,
|
|
NULL,
|
|
p,
|
|
NULL,
|
|
NULL,
|
|
TargetInfDirectory,
|
|
p,
|
|
0)) {
|
|
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupQueueCopy failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
|
|
}
|
|
|
|
if(NonNativeSearch) {
|
|
//
|
|
// have to go around the houses here
|
|
// since SetupDiInstallDriverFiles does not support alternate platform
|
|
//
|
|
DriverInfoDetailData.cbSize = sizeof(SP_DRVINFO_DETAIL_DATA);
|
|
|
|
if(!SetupDiGetDriverInfoDetail(DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
&DriverInfoData,
|
|
&DriverInfoDetailData,
|
|
sizeof(DriverInfoDetailData),
|
|
NULL)) {
|
|
//
|
|
// If we failed with ERROR_INSUFFICIENT_BUFFER, then that's OK.
|
|
//
|
|
Err = GetLastError();
|
|
if(Err != ERROR_INSUFFICIENT_BUFFER) {
|
|
_tprintf(TEXT("SetupDiGetDriverInfoDetail failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
}
|
|
hInf = SetupOpenInfFile(DriverInfoDetailData.InfFileName,NULL,INF_STYLE_WIN4,NULL);
|
|
if(hInf == INVALID_HANDLE_VALUE) {
|
|
_tprintf(TEXT("SetupOpenInfFile %s failed with %lx\n"),DriverInfoDetailData.InfFileName, GetLastError());
|
|
goto clean1;
|
|
}
|
|
if(!GetActualSectionToInstallEx(hInf,
|
|
DriverInfoDetailData.SectionName,
|
|
AltPlatformInfo.ProcessorArchitecture,
|
|
ActualSectionName,
|
|
LINE_LEN)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("GetActualSectionToInstallEx failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
Err = FilesFromInfSectionAndNeededSections(hInf,
|
|
SourcePath,
|
|
ActualSectionName,
|
|
DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
QueueHandle);
|
|
if(Err) {
|
|
_tprintf(TEXT("FilesFromInfSectionAndNeededSections failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
lstrcpy(ActualSectionName2,ActualSectionName);
|
|
lstrcat(ActualSectionName2,TEXT(".CoInstallers"));
|
|
Err = FilesFromInfSectionAndNeededSections(hInf,
|
|
SourcePath,
|
|
ActualSectionName2,
|
|
DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
QueueHandle);
|
|
if(Err) {
|
|
_tprintf(TEXT("FilesFromInfSectionAndNeededSections failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
lstrcpy(ActualSectionName2,ActualSectionName);
|
|
lstrcat(ActualSectionName2,TEXT(".Interfaces"));
|
|
Err = FilesFromInfSectionAndNeededSections(hInf,
|
|
SourcePath,
|
|
ActualSectionName2,
|
|
DeviceInfoSet,
|
|
&DeviceInfoData,
|
|
QueueHandle);
|
|
if(Err) {
|
|
_tprintf(TEXT("FilesFromInfSectionAndNeededSections failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
//
|
|
// done
|
|
//
|
|
SetupCloseInfFile(hInf);
|
|
} else if(DontCallClassInstaller) {
|
|
//
|
|
// appeneded to the queue we've associated with DeviceInfoSet
|
|
//
|
|
if(!SetupDiInstallDriverFiles(DeviceInfoSet, &DeviceInfoData)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiInstallDriverFiles failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
} else {
|
|
if(!SetupDiCallClassInstaller(DIF_INSTALLDEVICEFILES, DeviceInfoSet, &DeviceInfoData)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupDiCallClassInstaller(DIF_INSTALLDEVICEFILES) failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// OK, now we've got all the file operations in our file queue. Now scan
|
|
// the file queue and print out each file to be copied.
|
|
//
|
|
if(!SetupScanFileQueue(QueueHandle,
|
|
SPQ_SCAN_USE_CALLBACKEX|SPQ_SCAN_FILE_PRESENCE,
|
|
NULL,
|
|
FileQueueScanCallback,
|
|
NULL,
|
|
&ScanResult)) {
|
|
//
|
|
// Might have been because SPQ_SCAN_FILE_PRESENCE combo not understood
|
|
// try as a 2nd resort...
|
|
//
|
|
if(!SetupScanFileQueue(QueueHandle,
|
|
SPQ_SCAN_USE_CALLBACKEX,
|
|
NULL,
|
|
FileQueueScanCallback,
|
|
NULL,
|
|
&ScanResult)) {
|
|
Err = GetLastError();
|
|
_tprintf(TEXT("SetupScanFileQueue failed with %lx\n"), Err);
|
|
goto clean1;
|
|
}
|
|
}
|
|
|
|
//
|
|
// We're done! We've successfully printed out all files that are installed by this driver
|
|
// node.
|
|
//
|
|
|
|
clean1:
|
|
|
|
|
|
|
|
clean0:
|
|
|
|
SetupDiDestroyDeviceInfoList(DeviceInfoSet);
|
|
|
|
if (QueueHandle != INVALID_HANDLE_VALUE) {
|
|
SetupCloseFileQueue(QueueHandle);
|
|
}
|
|
|
|
//
|
|
// Clean up PNFs.
|
|
//
|
|
lstrcpy(SearchSpec + StringLen, TEXT("*.PNF"));
|
|
pSysSetupEnumerateFiles(SearchSpec, DeleteSinglePnf, NULL);
|
|
|
|
return ((Err == NO_ERROR) ? 0 : -1);
|
|
}
|
|
|
|
|
|
VOID
|
|
pSysSetupEnumerateFiles(
|
|
IN OUT PWSTR SearchSpec,
|
|
IN PFILEENUM_CALLBACK FileEnumCallback,
|
|
IN OUT PVOID Context
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine enumerates every (non-directory) file matching the specified wildcard, and
|
|
passes the filename (w/path) to the specified callback, along with the caller-supplied
|
|
context.
|
|
|
|
Arguments:
|
|
|
|
SearchSpec - Specifies the files to be enumerated (e.g., "C:\WINNT\INF\*.INF").
|
|
The character buffer pointed to must be at least MAX_PATH characters large.
|
|
THIS BUFFER IS USED AS WORKING SPACE BY THIS ROUTINE, AND ITS CONTENTS WILL
|
|
BE MODIFIED!
|
|
|
|
FileEnumCallback - Supplies the address of the callback routine to be called for each
|
|
file enumerated. The prototype of this function is:
|
|
|
|
typedef BOOL (*PFILEENUM_CALLBACK) {
|
|
IN PCTSTR Filename,
|
|
IN OUT PVOID Context
|
|
);
|
|
|
|
(Returning TRUE from the callback continues enumeration, FALSE aborts it.)
|
|
|
|
Context - Supplies a context that is passed to the callback for each file.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
PWSTR FilenameStart;
|
|
HANDLE FindHandle;
|
|
WIN32_FIND_DATA FindData;
|
|
|
|
FilenameStart = _tcsrchr(SearchSpec, TEXT('\\'));
|
|
FilenameStart++;
|
|
|
|
if((FindHandle = FindFirstFile(SearchSpec, &FindData)) != INVALID_HANDLE_VALUE) {
|
|
|
|
do {
|
|
//
|
|
// Skip directories
|
|
//
|
|
if(FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Form full pathname of file in SearchSpec.
|
|
//
|
|
lstrcpy(FilenameStart, FindData.cFileName);
|
|
|
|
//
|
|
// Call the callback for this file.
|
|
//
|
|
if(!FileEnumCallback(SearchSpec, Context)) {
|
|
//
|
|
// Callback aborted enumeration.
|
|
//
|
|
break;
|
|
}
|
|
|
|
} while(FindNextFile(FindHandle, &FindData));
|
|
|
|
FindClose(FindHandle);
|
|
}
|
|
}
|
|
|
|
|
|
BOOL
|
|
PrecompileSingleInf(
|
|
IN PCTSTR FullInfPath,
|
|
IN OUT PVOID Context
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine precompiles the specified INF.
|
|
|
|
Arguments:
|
|
|
|
FullInfPath - Supplies the name of the INF to be precompiled.
|
|
|
|
Context - Supplies a pointer to a boolean. If non-zero, caching is enabled,
|
|
otherwise it's disabled (i.e., the corresponding PNF is deleted).
|
|
|
|
Return Value:
|
|
|
|
TRUE to continue enumeration, FALSE to abort it (we always return TRUE).
|
|
|
|
--*/
|
|
{
|
|
HINF hInf;
|
|
|
|
hInf = SetupOpenInfFile(FullInfPath,
|
|
NULL,
|
|
INF_STYLE_WIN4 | (*((PBOOL)Context) ? INF_STYLE_CACHE_ENABLE : INF_STYLE_CACHE_DISABLE),
|
|
NULL
|
|
);
|
|
|
|
if(hInf != INVALID_HANDLE_VALUE) {
|
|
SetupCloseInfFile(hInf);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOL
|
|
DeleteSinglePnf(
|
|
IN PCTSTR FullPnfPath,
|
|
IN OUT PVOID Context
|
|
)
|
|
{
|
|
UNREFERENCED_PARAMETER(Context);
|
|
|
|
DeleteFile(FullPnfPath);
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
#if 1
|
|
|
|
UINT
|
|
FileQueueScanCallback(
|
|
IN PVOID Context,
|
|
IN UINT Notification,
|
|
IN UINT_PTR Param1,
|
|
IN UINT_PTR Param2
|
|
)
|
|
{
|
|
PFILEPATHS FilePaths = (PFILEPATHS)Param1;
|
|
PTSTR Source, Target;
|
|
|
|
if(Notification == SPFILENOTIFY_QUEUESCAN_EX) {
|
|
|
|
#if FULL_FILEPATHS_SPEW
|
|
_tprintf(TEXT("%s -> %s (err %lx, flags %lx)\n"),
|
|
FilePaths->Source,
|
|
FilePaths->Target,
|
|
FilePaths->Win32Error,
|
|
FilePaths->Flags
|
|
);
|
|
#else
|
|
//
|
|
// Get simple filenames for source and target...
|
|
//
|
|
Source = _tcsrchr(FilePaths->Source, TEXT('\\'));
|
|
if(!Source) {
|
|
_tprintf(TEXT("ERROR! Full source path not included for %s--aborting!\n"), FilePaths->Source);
|
|
return ERROR_PATH_NOT_FOUND;
|
|
}
|
|
|
|
Target = _tcsrchr(FilePaths->Target, TEXT('\\'));
|
|
if(!Target) {
|
|
_tprintf(TEXT("ERROR! Full target path not included for %s--aborting!\n"), FilePaths->Target);
|
|
return ERROR_PATH_NOT_FOUND;
|
|
}
|
|
|
|
Source++;
|
|
Target++;
|
|
|
|
_tprintf(TEXT("%s\n"), Source);
|
|
|
|
#endif // FULL_FILEPATHS_SPEW
|
|
|
|
}
|
|
|
|
return NO_ERROR;
|
|
}
|
|
|
|
#else
|
|
|
|
UINT
|
|
DriverListMsgHandler(
|
|
IN PVOID Context,
|
|
IN UINT Notification,
|
|
IN UINT_PTR Param1,
|
|
IN UINT_PTR Param2
|
|
)
|
|
{
|
|
UINT ret = FILEOP_SKIP;
|
|
PFILEPATHS FilePaths = (PFILEPATHS)Param1;
|
|
|
|
UNREFERENCED_PARAMETER(Context);
|
|
UNREFERENCED_PARAMETER(Param2);
|
|
|
|
if(Notification == SPFILENOTIFY_STARTCOPY) {
|
|
//
|
|
// Print out the source filename...
|
|
//
|
|
_tprintf(TEXT("%s ( "), FilePaths->Source);
|
|
|
|
//
|
|
// Now print out any flags set for this copy operation.
|
|
//
|
|
|
|
//
|
|
// First, here are the flags that can't come from an INF (but the
|
|
// class installer might have set them)...
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_DELETESOURCE) {
|
|
_tprintf(TEXT("DELETESOURCE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_NOOVERWRITE) {
|
|
_tprintf(TEXT("NOOVERWRITE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_LANGUAGEAWARE) {
|
|
_tprintf(TEXT("LANGUAGEAWARE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_SOURCE_ABSOLUTE) {
|
|
_tprintf(TEXT("SOURCE_ABSOLUTE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_SOURCEPATH_ABSOLUTE) {
|
|
_tprintf(TEXT("SOURCEPATH_ABSOLUTE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_NOBROWSE) {
|
|
_tprintf(TEXT("NOBROWSE "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_SOURCE_SIS_MASTER) {
|
|
_tprintf(TEXT("SOURCE_SIS_MASTER "));
|
|
}
|
|
|
|
//
|
|
// Now for the flags that are settable via an INF...
|
|
//
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_REPLACEONLY bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_REPLACEONLY) {
|
|
_tprintf(TEXT("REPLACEONLY "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_NOVERSIONCHECK bit? If so, then
|
|
// the SP_COPY_NEWER_OR_SAME bit will be _cleared_.
|
|
// (The class-installer may have also cleared this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_NEWER_OR_SAME) { // same as SP_COPY_NEWER
|
|
_tprintf(TEXT("NEWER_OR_SAME "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_NODECOMP bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_NODECOMP) {
|
|
_tprintf(TEXT("NODECOMP "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_FORCE_FILE_IN_USE bit? If so, then
|
|
// both the SP_COPY_IN_USE_NEEDS_REBOOT and SP_COPY_FORCE_IN_USE flags
|
|
// will be set.
|
|
// (The class-installer may have also set these flags, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_IN_USE_NEEDS_REBOOT) {
|
|
_tprintf(TEXT("IN_USE_NEEDS_REBOOT "));
|
|
}
|
|
if(FilePaths->Flags & SP_COPY_FORCE_IN_USE) {
|
|
_tprintf(TEXT("FORCE_IN_USE "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_NOSKIP bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_NOSKIP) {
|
|
_tprintf(TEXT("NOSKIP "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_NO_OVERWRITE bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_FORCE_NOOVERWRITE) {
|
|
_tprintf(TEXT("FORCE_NOOVERWRITE "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_NO_VERSION_DIALOG bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_FORCE_NEWER) {
|
|
_tprintf(TEXT("FORCE_NEWER "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_WARN_IF_SKIP bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_WARNIFSKIP) {
|
|
_tprintf(TEXT("WARNIFSKIP "));
|
|
}
|
|
|
|
//
|
|
// Did the INF specify the COPYFLG_OVERWRITE_OLDER_ONLY bit?
|
|
// (The class-installer may have also set this flag, but it's unlikely.)
|
|
//
|
|
if(FilePaths->Flags & SP_COPY_NEWER_ONLY) {
|
|
_tprintf(TEXT("NEWER_ONLY "));
|
|
}
|
|
|
|
//
|
|
// Get ready for next line...
|
|
//
|
|
_tprintf(TEXT(")\n"));
|
|
|
|
} else if(Notification == SPFILENOTIFY_NEEDMEDIA) {
|
|
lstrcpy((PTSTR)Param2, SourcePath);
|
|
ret = FILEOP_NEWPATH;
|
|
}
|
|
|
|
//
|
|
// Since FILEOP_SKIP is a nonzero value, it's OK to always return this.
|
|
//
|
|
return ret;
|
|
}
|
|
|
|
#endif
|
|
|
|
DWORD
|
|
FilesFromInfSectionAndNeededSections(
|
|
IN HINF InfHandle,
|
|
IN PCTSTR InfPath,
|
|
IN PCTSTR SectionName,
|
|
IN HDEVINFO DeviceInfoSet, OPTIONAL
|
|
IN PSP_DEVINFO_DATA DeviceInfoData, OPTIONAL
|
|
IN HSPFILEQ UserFileQ OPTIONAL
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Process Copy directives from specified section
|
|
taking note of Include/Needs
|
|
|
|
Return Value:
|
|
|
|
If successful, the return value is NO_ERROR, otherwise, it is a Win32 error code
|
|
indicating the cause of the failure.
|
|
|
|
--*/
|
|
{
|
|
DWORD FieldIndex, Err;
|
|
INFCONTEXT InfContext;
|
|
BOOL NeedsEntriesToProcess;
|
|
TCHAR SectionToInstall[LINE_LEN];
|
|
TCHAR InfFullPath[MAX_PATH];
|
|
int FileLen;
|
|
|
|
|
|
//
|
|
// Store the full directory path to where the supplied INF is located, so we
|
|
// can first attempt to append-load the included INFs from that same directory.
|
|
//
|
|
lstrcpyn(InfFullPath, InfPath, MAX_PATH-3);
|
|
FileLen = lstrlen(InfFullPath);
|
|
if((*CharPrev(InfFullPath,InfFullPath+FileLen)!=TEXT('\\')) &&
|
|
(*CharPrev(InfFullPath,InfFullPath+FileLen)!=TEXT('/'))) {
|
|
InfFullPath[FileLen++] = TEXT('\\');
|
|
}
|
|
|
|
if(SetupFindFirstLine(InfHandle, SectionName, TEXT("include"), &InfContext)) {
|
|
|
|
for(FieldIndex = 1;
|
|
SetupGetStringField(&InfContext,
|
|
FieldIndex,
|
|
InfFullPath+FileLen,
|
|
(DWORD)(MAX_PATH-FileLen),
|
|
NULL);
|
|
FieldIndex++)
|
|
{
|
|
//
|
|
// Try only full path, if that fails, tough
|
|
//
|
|
SetupOpenAppendInfFile(InfFullPath, InfHandle, NULL);
|
|
}
|
|
}
|
|
SetupOpenAppendInfFile(NULL, InfHandle, NULL);
|
|
|
|
|
|
lstrcpyn(SectionToInstall, SectionName, LINE_LEN);
|
|
|
|
NeedsEntriesToProcess = SetupFindFirstLine(InfHandle,
|
|
SectionName,
|
|
TEXT("needs"),
|
|
&InfContext
|
|
);
|
|
|
|
Err = NO_ERROR;
|
|
|
|
for(FieldIndex = 0; (!FieldIndex || NeedsEntriesToProcess); FieldIndex++) {
|
|
|
|
if(FieldIndex) {
|
|
//
|
|
// Get next section name on "needs=" line to be processed.
|
|
//
|
|
if(!SetupGetStringField(&InfContext,
|
|
FieldIndex,
|
|
SectionToInstall,
|
|
LINE_LEN,
|
|
NULL)) {
|
|
//
|
|
// We've exhausted all the extra sections we needed to install.
|
|
//
|
|
break;
|
|
}
|
|
}
|
|
|
|
SetupInstallFilesFromInfSection(InfHandle,NULL,UserFileQ,SectionToInstall,NULL,0);
|
|
}
|
|
|
|
return Err;
|
|
}
|
|
|
|
BOOL
|
|
GetActualSectionToInstallEx(
|
|
IN HINF InfHandle,
|
|
IN PCTSTR InfSectionName,
|
|
IN WORD ProcessorArchitecture,
|
|
OUT PTSTR InfSectionWithExt,
|
|
IN DWORD InfSectionWithExtSize
|
|
)
|
|
{
|
|
//
|
|
// Poor mans SetupDiGetActualSectionToInstallEx
|
|
//
|
|
DWORD SectionNameLen = (DWORD)lstrlen(InfSectionName);
|
|
DWORD ExtBufferLen;
|
|
BOOL ExtFound = TRUE;
|
|
DWORD Err = NO_ERROR;
|
|
DWORD Platform;
|
|
PCTSTR NtArchSuffix;
|
|
DWORD NtArchSuffixSize;
|
|
|
|
switch(ProcessorArchitecture) {
|
|
|
|
case PROCESSOR_ARCHITECTURE_INTEL :
|
|
NtArchSuffix = TEXT(".ntx86");
|
|
break;
|
|
|
|
case PROCESSOR_ARCHITECTURE_IA64 :
|
|
NtArchSuffix = TEXT(".ntia64");
|
|
break;
|
|
|
|
case PROCESSOR_ARCHITECTURE_AMD64 :
|
|
NtArchSuffix = TEXT(".ntamd64");
|
|
break;
|
|
|
|
default:
|
|
//
|
|
// Unknown/invalid architecture
|
|
//
|
|
SetLastError(ERROR_INVALID_PARAMETER);
|
|
return FALSE;
|
|
}
|
|
NtArchSuffixSize = (DWORD)lstrlen(NtArchSuffix)+1;
|
|
if((SectionNameLen+NtArchSuffixSize)>InfSectionWithExtSize) {
|
|
SetLastError(ERROR_INSUFFICIENT_BUFFER);
|
|
return FALSE;
|
|
}
|
|
lstrcpy(InfSectionWithExt,InfSectionName);
|
|
lstrcat(InfSectionWithExt,NtArchSuffix);
|
|
if(SetupGetLineCount(InfHandle, InfSectionWithExt) != -1) {
|
|
return TRUE;
|
|
}
|
|
lstrcpy(InfSectionWithExt,InfSectionName);
|
|
lstrcat(InfSectionWithExt,TEXT(".nt"));
|
|
if(SetupGetLineCount(InfHandle, InfSectionWithExt) != -1) {
|
|
return TRUE;
|
|
}
|
|
lstrcpy(InfSectionWithExt,InfSectionName);
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
VOID
|
|
Usage(
|
|
VOID
|
|
)
|
|
{
|
|
_tprintf(TEXT("Usage: DRVLIST [/S] [/I] [/F CertClassInfPath] [/A arch] [/V ver] SourcePath\n\n"));
|
|
_tprintf(TEXT("Options:\n\n"));
|
|
_tprintf(TEXT("/I Include INFs in the list\n"));
|
|
_tprintf(TEXT("/S Skip class-/co-installers; build list strictly from INFs\n"));
|
|
_tprintf(TEXT("/F Filter based on list of class GUIDs in [DriverSigningClasses]\n"));
|
|
_tprintf(TEXT(" section of specified INF\n"));
|
|
_tprintf(TEXT("/A Architecture to build for, eg '/A 6' (IA64). Assumes /S\n"));
|
|
_tprintf(TEXT("/V Version to build for, eg '/V 0501' (5.1). Assumes /S\n"));
|
|
_tprintf(TEXT("/? or /H Display brief usage message\n"));
|
|
}
|
|
|