How can I give an old ActiveX control new GUIDs? - c++

I am trying to modify an ActiveX control developed in Visual Studio 2008 to use it for a purpose for which it was not originally designed. I will be reusing at least 90% of its code. Therefore, I would like to begin by creating an identical control that uses different GUIDs. I tried to follow instructions I found here (a very old link, written in 2004), but when I tried to build my project, I got an assertion failure in ctlreg.cpp line 113. Then, I restored all my changed files back to their original states, and for each GUID is the .odl file, I searched for the GUID in my .cpp and .h files and changed it wherever I found it. I also made sure to change my major version number. I still get the assertion failure. What else should I be doing?
Here's the code from ctlreg.cpp from the start of the method containing the assertion to the assertion itself:
BOOL AFXAPI AfxOleRegisterTypeLib(HINSTANCE hInstance, REFGUID tlid,
LPCTSTR pszFileName, LPCTSTR pszHelpDir)
{
BOOL bSuccess = FALSE;
CStringW strPathNameW;
wchar_t *szPathNameW = strPathNameW.GetBuffer(_MAX_PATH);
::GetModuleFileNameW(hInstance, szPathNameW, _MAX_PATH);
strPathNameW.ReleaseBuffer();
LPTYPELIB ptlib = NULL;
// If a filename was specified, replace final component of path with it.
if (pszFileName != NULL)
{
int iBackslash = strPathNameW.ReverseFind('\\');
if (iBackslash != -1)
strPathNameW = strPathNameW.Left(iBackslash+1);
strPathNameW += pszFileName;
}
if (SUCCEEDED(LoadTypeLib(strPathNameW.GetString(), &ptlib)))
{
ASSERT_POINTER(ptlib, ITypeLib);
LPTLIBATTR pAttr;
GUID tlidActual = GUID_NULL;
if (SUCCEEDED(ptlib->GetLibAttr(&pAttr)))
{
ASSERT_POINTER(pAttr, TLIBATTR);
tlidActual = pAttr->guid;
ptlib->ReleaseTLibAttr(pAttr);
}
// Check that the guid of the loaded type library matches
// the tlid parameter.
ASSERT(IsEqualGUID(tlid, tlidActual));

Related

How to create a list of Keyboards. Extract KLID from HKL?

Windows 10 / C++ / Win32
I need to make a list of 'installed' Keyboards.
The components of my 'list' needs to contain:
The HKL for the keyboard.
The KLID for the keyboard.
The values of the registry values obtained from:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts
Example: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00000409
Value: Layout File
Value: Layout Text
The only way I am aware of enumerating Keyboards is via GetKeyboardLayoutList(), which returns a list of HKL's.
Methodology which works for 'standard' keyboards (HKL's 04090409, 04070407,..).
TCHAR Buffer[50];
TCHAR Buffer2[50];
HKL LoadedKeyboardLayout;
// Hkl: 04090409
_stprintf_s(Buffer, (sizeof(Buffer)/sizeof(TCHAR)), _T("%08X"), (((UINT)Hkl >> 16) & 0xFFFF));
// Buffer: "0000409"
LoadedKeyboardLayout = LoadKeyboardLayout(Buffer, (UINT)0);
// It Loads
ActivateKeyboardLayout(LoadedKeyboardLayout, KLF_SETFORPROCESS);
// It Activates
GetKeyboardLayoutName(Buffer2);
// Buffer2: "00000409"
// I can now fish the registry HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00000409
This DOES not work when the 'device identifier' is NOT 0000.
From LoadKeyboardLayout() documentation:
The name of the input locale identifier to load. This name is a string composed of the hexadecimal value of the Language Identifier (low word) and a device identifier (high word). For example, U.S. English has a language identifier of 0x0409, so the primary U.S. English layout is named "00000409". Variants of U.S. English layout (such as the Dvorak layout) are named "00010409", "00020409", and so on.
If one creates a 'Custom' keyboard, I find it is impossible to get the 'device identifier' from the custom keyboards HKL.
For instance:
Create a Custom Keyboard US with MKLCS.
Install it.
The 'KLID' will be 'A0000409'.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\a0000409
The HKL will be 0xF0C00409 (as returned within the HKL list generated by GetKeyboardLayoutList()).
In order to load the keyboard with LoadKeyboardLayout(), one needs 'A0000409'.
It does not seem possible to create A0000409 from F0C00409.
I also created my own keyboard layout without MKLCS.
I arbitrarily named it 00060409.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\00060409
It's HKL is FFFE0409 (as returned within the HKL list generated by GetKeyboardLayoutList()).
It does not seem possible to create 00060409 from FFFE0409.
With all of that said, how does one obtain a KLID from an HKL?
Or, is there another way I can go about creating my list of installed keyboards?
//================================================================
11/25/2020 Addition.
Thank you Rita.
It seems as if GetKeyboardLayoutList() creates a list of 'Loaded' keyboard layouts.
System loaded keyboard layouts, 0x0409409,....
Keyboard Layouts installed via MKLCS installation.
It seems that any Keyboards defined within the following registry keys will get loaded at boot.
HKEY_CURRENT_USER\Keyboard Layout
Note the Preload and Substitute value's.
(Note there are many other 'Keyboard Layout' keys spread around within the registry, so I am
not sure if HKEY_CURRENT_USER\Keyboard Layout is the actual registry key that defines the
PreLoad - Substitutes.
HKEY_USERS\.DEFAULT\Keyboard Layout
HKEY_USERS\S-1-5-18 (My User Account)
HKEY_LOCAL_MACHINE\SYSTEM\Keyboard Layout
)
So there is no doubt in my mind that Rita's example works due to the fact that her HKEY_CURRENT_USER\Keyboard Layout contains the following entries:
Preload:
1 d0010804
Substitutes:
d0010804 a0000804
The entries may have been put there by the MKLCS installer?
Or perhaps the action of adding the keyboard via Settings->Time & Language->Click on Preferred Language->Options->Add a Keyboard
From ActivateKeyboardLayout() documentation:
The input locale identifier must have been loaded by a previous call to the LoadKeyboardLayout function.
Since a0000804 (HKL: F0C00804) is actually already loaded ActivateKeyboardLayout() works.
Since my KLID: 00060409 is not referenced within any of the X\Keyboard Layout Preload and Substitutes
I must physically call LoadKeyBoardLayout(L"00060409") in order for it to appear within the GetKeyboardList() HKL's.
Thanks again Rita.
how does one obtain a KLID from an HKL?
There seems no direct method out of box to achieve it.
A workaround is retrieving a list of HKLs, then active a layout via a HKL, then get its KLID. The following is an example:
int cnt = GetKeyboardLayoutList(sizeof(hklArr)/sizeof(hklArr[0]), hklArr);
if(cnt > 0)
{
printf("keyboard list: \n");
for (UINT i = 0; i < cnt; i++)
{
printf("%x\n", (LONG_PTR)hklArr[i]);
if (ActivateKeyboardLayout(hklArr[i], KLF_SETFORPROCESS))
{
WCHAR pName[KL_NAMELENGTH];
if (GetKeyboardLayoutName(pName))
{
wprintf(L"layout name (KLID): %s\n", pName);
}
}
}
}
Result like this:
Update:
Update 2: Share my creating and installing steps.
I use Keyboard Layout Creator 1.4.
Load an existing keyboard for testing purpose. (You can modify based on it or create your own completely.)
Valid and test keyboard to make sure it works as you expected. Then build DLL and setup package.
Run setup.exe generated by step 2. After installation complete you will see the related preload keyboard layout item in registry.
There is no safe way to do this since it is not documented and can be changed after Windows updates.
Proper algorithm is posted here.
Here is my code that is working on my Windows 10 Version 21H2 (November 2021 Update) so far:
// Returns KLID string of size KL_NAMELENGTH
// Same as GetKeyboardLayoutName but for any HKL
// https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values
BOOL GetKLIDFromHKL(HKL hkl, _Out_writes_(KL_NAMELENGTH) LPWSTR pwszKLID)
{
bool succeded = false;
if ((HIWORD(hkl) & 0xf000) == 0xf000) // deviceId contains layoutId
{
WORD layoutId = HIWORD(hkl) & 0x0fff;
HKEY key;
CHECK_EQ(::RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key), ERROR_SUCCESS);
DWORD index = 0;
while (::RegEnumKeyW(key, index, pwszKLID, KL_NAMELENGTH) == ERROR_SUCCESS)
{
WCHAR layoutIdBuffer[MAX_PATH] = {};
DWORD layoutIdBufferSize = sizeof(layoutIdBuffer);
if (::RegGetValueW(key, pwszKLID, L"Layout Id", RRF_RT_REG_SZ, nullptr, layoutIdBuffer, &layoutIdBufferSize) == ERROR_SUCCESS)
{
if (layoutId == std::stoul(layoutIdBuffer, nullptr, 16))
{
succeded = true;
DBGPRINT("Found KLID 0x%ls by layoutId=0x%04x", pwszKLID, layoutId);
break;
}
}
++index;
}
CHECK_EQ(::RegCloseKey(key), ERROR_SUCCESS);
}
else
{
WORD langId = LOWORD(hkl);
// deviceId overrides langId if set
if (HIWORD(hkl) != 0)
langId = HIWORD(hkl);
std::swprintf(pwszKLID, KL_NAMELENGTH, L"%08X", langId);
succeded = true;
DBGPRINT("Found KLID 0x%ls by langId=0x%04x", pwszKLID, langId);
}
return succeded;
}
Also you can use this code to enumerate layout profiles with KLID in LAYOUTORTIPPROFILE.szId:
typedef struct tagLAYOUTORTIPPROFILE {
DWORD dwProfileType;
LANGID langid;
CLSID clsid;
GUID guidProfile;
GUID catid;
DWORD dwSubstituteLayout;
DWORD dwFlags;
WCHAR szId[MAX_PATH];
} LAYOUTORTIPPROFILE;
// Flags used in LAYOUTORTIPPROFILE::dwProfileType
#define LOTP_INPUTPROCESSOR 1
#define LOTP_KEYBOARDLAYOUT 2
// Flags used in LAYOUTORTIPPROFILE::dwFlags.
#define LOT_DEFAULT 0x0001
#define LOT_DISABLED 0x0002
std::vector<LAYOUTORTIPPROFILE> EnumLayoutProfiles()
{
// http://archives.miloush.net/michkap/archive/2008/09/29/8968315.html
// https://learn.microsoft.com/en-us/windows/win32/tsf/enumenabledlayoutortip
typedef UINT(WINAPI* EnumEnabledLayoutOrTipFunc)(LPCWSTR pszUserReg, LPCWSTR pszSystemReg, LPCWSTR pszSoftwareReg, LAYOUTORTIPPROFILE* pLayoutOrTipProfile, UINT uBufLength);
static EnumEnabledLayoutOrTipFunc EnumEnabledLayoutOrTip = reinterpret_cast<EnumEnabledLayoutOrTipFunc>(::GetProcAddress(::LoadLibraryA("input.dll"), "EnumEnabledLayoutOrTip"));
if (!EnumEnabledLayoutOrTip)
return {};
const UINT count = EnumEnabledLayoutOrTip(nullptr, nullptr, nullptr, nullptr, 0);
std::vector<LAYOUTORTIPPROFILE> layouts;
layouts.resize(count);
const UINT written = EnumEnabledLayoutOrTip(nullptr, nullptr, nullptr, layouts.data(), count);
CHECK_EQ(count, written);
return layouts;
}
The LAYOUTORTIPPROFILE.szId string format of the layout is:
<LangID>:<KLID>
The string format of the text service profile (IME) is:
<LangID>:{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
More info here.

Where does MFC COleControl::DoPropExchange store persistent properties?

I have taken over maintenance of a legacy MFC OCX control in C++. The project is now in VS2013. I'm trying to understand the functioning of the DoPropExchange method. This method appears to be calling PX_?????(member) for nearly all the data members in the control where ???? is the type (Bool, Short, Long ...) My understanding is these are called for the purpose of providing persistent storage of properties. But from my understanding of the operation of the OCX control there are no persistent properties. Would there be any other reason to be calling PX_???? for all data members in DoPropExchange other than to support persistent properties? I'm also trying to understand where these persistent properties are loaded/stored. Where is the serialized file for loading/storing persistent property values specified?
Here is the source for DoPropExchange
// CSigPlusCtrl::DoPropExchange - Persistence support
void CSigPlusCtrl::DoPropExchange(CPropExchange* pPX)
{
DWORD Version;
long BaudRate;
short ComPort;
BOOL Rv;
LOG(("DoPropExchange Entry"));
ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
COleControl::DoPropExchange(pPX);
Version = pPX->GetVersion();
if (pPX->IsLoading())
{
LoadDefaultProperties();
LoadIniParameters();
}
if ((Version & 0xFFFF0000) == (DWORD)MAKELONG(0, _wVerMajor))
{
Rv = PX_Short(pPX, _T("ImageFileFormat"), ImageFileFormat, 0);
Rv = PX_Short(pPX, _T("ImageXSize"), ImageXSize, 0);
Rv = PX_Short(pPX, _T("ImageYSize"), ImageYSize, 0);
Rv = PX_Short(pPX, _T("ImagePenWidth"), ImagePenWidth, 1);
. . .
Rv = PX_Short(pPX, _T("ZoomY"), ZoomY, 0);
Rv = PX_Short(pPX, _T("ZoomPower"), ZoomPower, 1);
if (pPX->IsLoading())
{
if (SigBlob != NULL)
{
GlobalFree(SigBlob);
SigBlob = NULL;
}
}
else
{
if (SigBlob == NULL)
{
SigBlobType* SigBlobPtr;
SigBlob = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
SigBlobPtr = (SigBlobType*)GlobalLock(SigBlob);
SigBlobPtr->Size = 0;
GlobalUnlock(SigBlob);
}
}
if ((Version & 0xFFFF) == Version223)
{
Rv = PX_Blob(pPX, _T("SigBlob"), SigBlob, NULL);
}
if ((Version & 0xFFFF) >= Version224)
{
CString SigStr;
if (!pPX->IsLoading())
{
SigStr = BlobToString();
}
Rv = PX_String(pPX, _T("SigStringStored"), SigStr, _T(""));
if (pPX->IsLoading())
{
BlobFromString(SigStr);
}
}
}
else
{
SigMessageBox("Warning Incompatable Versions of SigPlus Control");
}
LoadTabletParameters();
LOG(("DoPropExchange Exit"));
}
EDIT Added 6-21-2018
Running in the debugger I observe that when DoPropExchange is called, VS2013 shows the stack with a message that stack frames below may be incorrect. And the one frame just above, that calls DoPropExchange, is from mfc120d.dll which does not have symbol file available mfc120d.i386.pdb.
This Microsoft Forum Post seems to indicate that the symbol file is not available for VS2015 and I'm wondering if that is also the case for VS2013. So far I have not been able to find place to download MFC120 symbols for debug.
Starting a bounty today to find someone who can explain how and where properties are normally serialized for OLE controls and what methods are used to specify the serialized data storage location/media. This is of concern because this control runs in a Citrix ZenDesk network environment in a Terminal Aware program and if properties are being stored somewhere then each client needs to specify a location unique to that client.
The DoPropExchange is used to implement control attribute persistence mainly between design and run-time. The actual destination sink is passed by the client of the OCX.
In VC the the settings are stored in RC file while in VB in frm and frx files. If you open FRM in notepad you would probably see the section with the properties of this control.
As a side note, there is a similar implementation in case the control is used in HTML with inline settings in he html itself.
Unless your VB clients saves the settings externally via direct calls to Property bag function it is not likely you have a issue here since the above properties are not stored at runtime.

Saving printer properties in XE4 c++ VCL application

I am trying to save some of the printer properties (ie page size) to an ini file in the following way:
Printer()->PrinterIndex = Printer()->PrinterIndex;
wchar_t printerDevice[256], printerDriver[256], printerPort[256];
THandle printerDeviceModeHandle = 0;
DEVMODE *printerDeviceMode;
Printer()->GetPrinter(printerDevice, printerDriver, printerPort, printerDeviceModeHandle);
if (printerDeviceModeHandle != 0)
{
DEVMODE *printerDeviceMode = (DEVMODE *) GlobalLock((void *)printerDeviceModeHandle);
if ((printerDeviceMode != NULL) && (printerDeviceMode->dmFields & DM_PAPERSIZE))
ini->WriteInteger("PrintingPage", "PaperSize", printerDeviceMode->dmPaperSize);
GlobalUnlock((void *)printerDeviceModeHandle);
}
The code gets within the inner if statement but When I inspect the printerDeviceMode variable only the dmDeviceName appears to be correct. Many of the other settings appear to be garbage (including dmPaperSize):
Note that dmSize is 0.
I have tried changing the page size for the printer using a TPrinterSetupDialog components but the value of dmPaperSize stays the same.
Have I missed a step somewhere?
This is a XE4 c++ builder VCL project.

c++ MFC SDI DLL call method of OLE Server using IDispatch

I have written a c++ MFC DLL that brings up an SDI Application which is a very legacy OLE Server. (I have no choice about using this OLE Server so I have to make it work.)
I am accessing this c++ DLL from C#.
I have everything "working". I can call methods of the DLL, I have implemented C# delegates correctly, etc.
I can call methods of the OLE Server directly in C++ and export these so that my C# application call call them too. This is my "Hello World" for accessing the OLE Server functionality in its entirety from C#.
So far so good.
The next step is to make this C++ DLL as much of a "pass-through" as possible so that C# developers can write business logic around this OLE server. If changes or updates happen from the maker of the OLE Server, we do not want to have to update C++ code, we want to respond to the changes in C#.
So even though I can implement methods in C++ using the imported class from the OLEServer.tlb file and pass these through to C#, we do not want to ultimately do this. I am trying to call methods through the IDispatch interface instead and I am running into difficulties that I can't quite understand.
I am using Visual Studio 2010 for both unmanaged C++ and C#.
VARIANT LaserCat::LaserCatCommand(LPCTSTR* p_pMethodNameAndParamsInReverseOrder, UINT p_Count)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
VARIANT result;
VARIANT* pResult = NULL;
VariantInit(&result);
HRESULT hr = NULL;
DISPID dispid;
const IID IID_ITriad = {0x60EE772D,0xE076,0x4F58,{0xA8,0xB4,0x2F,0x7A,0x29,0xBB,0x02,0x50}};
COleException* pError = NULL;
BOOL HasDispatch = theApp.pTriadView->pTriadItem->Catalog.CreateDispatch(IID_ITriad, pError);
LPDISPATCH iDisp = theApp.pTriadView->pTriadItem->Catalog.m_lpDispatch;
LPOLESTR Names[1] = {(LPOLESTR)L"GetInterfaceVersion"};
hr = iDisp->GetIDsOfNames(IID_NULL, Names, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (hr != S_OK) return result;
DISPPARAMS* pParams = new DISPPARAMS();
short maj = 0;
short min = 0;
short* nMajor = &maj;
short* nMinor = &min;
VARIANTARG Args[2];
VariantInit(&Args[0]);
VariantInit(&Args[1]);
Args[0].piVal = nMinor;
Args[0].vt = VT_BYREF;
Args[1].piVal = nMajor;
Args[1].vt = VT_BYREF;
pParams->rgvarg = Args;
pParams->cNamedArgs = 0;
pParams->cArgs = 2;
pParams->rgdispidNamedArgs = NULL;
EXCEPINFO* pExcept = NULL;
UINT* pArgErrorIndex = NULL;
LPCTSTR Error = NULL;
hr = iDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, pParams, pResult, pExcept, pArgErrorIndex);
if (pExcept != NULL || pArgErrorIndex != NULL || hr != S_OK)
{
Error = _T("Error");
return result;
}
result = *pResult;
return result;
}
The following line from above gives me a "Bad variable type" error:
hr = iDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, pParams, pResult, pExcept, pArgErrorIndex);
BTW, I am sure that the code can be vastly improved, it has been years since I have written in C++ and this is just a "first kick" at getting this working so no real error handling etc.
I have toyed around with "Args[]" type and value with variations of "Type Mismatch" errors and "Null Reference Pointers"
The function that is imported by the .tlb file looks like this:
short GetInterfaceVersion(short * nMajor, short * nMinor)
{
short result;
static BYTE parms[] = VTS_PI2 VTS_PI2 ;
InvokeHelper(0x178, DISPATCH_METHOD, VT_I2, (void*)&result, parms, nMajor, nMinor);
return result;
}
Oh, although I am passing in the "pMethodNameAndParamsInReverseOrder" as a parameter, I am just hard-coding it to get this one simple method working. Once I have it working with this and a few other methods, I am planning on making this generic to handle any methods implemented by the COM interface via IDispatch.
Any help would be appreciated mostly in getting this working but I would also appreciate any pointers on improving the code, I have mouch to learn in C++
Thank-You!
BTW, If this helps clarify things, theApp.pTriadView->pTriadItem->Catalog is the COM class I am implementing
EDIT:
Thanks to #HansPassant (see first comment) I see what I was missing. Unfortunately I have hit a downstream result of fixing that. The VARIANT "pResult" is coming back empty. I will continue to hunt that down now but any thoughts would be welcome :)

How to get current directory of a specific drive in Visual Studio Express 2013(C++)?

I am porting a program from Borland C++ Builder to Visual Studio 2013 (C++). The program uses getcurdir to get the current directory of a drive. This function has a parameter drive, but the Microsoft equivalent function getcwd don't have such parameter. How can I do it?
As you tagged visual studio, I assume you'r using windows. Beside that current directory is just one, (i.e. where the executable is located or other if you've moved to) current directory won't be different depending on current drive, I think. Then, in windows you may use the function GetCurrentDirectory from winapi. The prototype is:
DWORD WINAPI GetCurrentDirectory(
_In_ DWORD nBufferLength,
_Out_ LPTSTR lpBuffer
);
You may get details here.
Example:
TCHAR cwd[100];
GetCurrentDirectory(100,cwd);
// now cwd will contain absolute path to current directory
(Yes I know this is an old entry, just for the records if somebody stumbles over the same issue...)
As deeiip already said correctly, in Windows there is only 1 current directory, but cmd.exe fakes DOS behavior when there was 1 current directory per drive.
If you need to access cmd's current directory per drive, use the according hidden environment variables, e.g. "%=C:%".
Here an example application (in C#):
using System;
static class Module1 {
public static void Main(String[] args) {
var myFolder = GetCurrentFolderPerDrive(args[0]); //e.g. "C:"
Console.WriteLine(myFolder);
}
private static string GetCurrentFolderPerDrive(string driveLetter) {
driveLetter = NormalizeDriveLetter(driveLetter);
string myEnvironmentVariable = $"%={driveLetter}%";
string myResult = Environment.ExpandEnvironmentVariables(myEnvironmentVariable);
if (myResult == myEnvironmentVariable) return $"{driveLetter.ToUpperInvariant()}\\"; //No current folder set, return root
return myResult;
}
private static String NormalizeDriveLetter(String driveLetter) {
if (String.IsNullOrWhiteSpace(driveLetter)) throw new ArgumentNullException(nameof(driveLetter), "The drive letter is null, empty or white-space.");
Boolean throwException = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".IndexOf(driveLetter[0]) < 0);
if (!throwException) {
if (driveLetter.Length == 1) {
driveLetter += ':';
} else if (driveLetter.Length != 2) {
throwException = true;
}
}
if (throwException) throw new ArgumentException($"A well-formed drive letter expected, e.g. \"C:\"!\r\nGiven value: \"{driveLetter}\".", nameof(driveLetter));
return driveLetter;
}
}