Check if everyone may read/write to a directory - delphi

We are facing the problem, that different user groups shall be able to read and write files from a common data directory (e.g. c:\ProgramData\xyz).
The data is written from different sources e.g. a service writes files into it and a user shall be able to change it's content later on.
The problem now is that this only works if "everybody" is allowed to read/write/change
files in that directory (und subdirs).
What I want to check in the installer is if all users are allowed to do so aka. check if the "every users" group (or "Jeder" group in german) is in the access list.
I have only basic knowledge about ACL and can change that in the explorer but I would need a few lines of code which push me into the right direction (in Delphi).
many thanks
Mike

I think it's not a Delphi but WinAPI question. Delphi doesn't have any special facilities to make this easier AFAIK.
Getting information from an ACL says you need to do GetSecurityInfo on an open handle, then GetEffectiveRightsFromACL on an ACL you get.
You specify a trustee, which can be by name, but better use SID. Name for "Everyone" can change, but there's a special SID for it which is valid on any PC, google it. Okay, here it is: "(S-1–1–0)". Or you can use CreateWellKnownSid and give it WinWorldSid to get the same SID (more proper but longer way).
All of that from five minutes of googling so watch out for mistakes.
Alright, here's some code.
function ConvertStringSidToSid(StringSid: PWideChar; var Sid: PSID): boolean; stdcall; external advapi32 name 'ConvertStringSidToSidW';
function AclGetEffectiveRights(const path, sid: string): cardinal;
var h: THandle; //handle to our directory
err: integer;
dacl: PACL; //access control list for the object
secdesc: pointer;
tr: TRUSTEE;
bsid: PSid;
begin
Result := 0;
//Open directory
h := CreateFile(PChar(path), GENERIC_READ, FILE_SHARE_READ, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_BACKUP_SEMANTICS, 0);
//we need FILE_FLAG_BACKUP_SEMANTICS to open a directory
if h=INVALID_HANDLE_VALUE then RaiseLastOsError();
try
bsid := nil;
//Query access control list for a directory -- the list you see in the properties box
err := GetSecurityInfo(h, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nil, nil, #dacl, nil, secdesc);
//GetSecurityInfo can return many things but we only need DACL,
//and we are required to also get a security descriptor
if err<>ERROR_SUCCESS then
raise Exception.CreateFmt('Cannot retrieve DACL: error %d',[err]);
try
//Convert string sid to binary sid
if not ConvertStringSidToSid(PChar(sid), bsid) then
RaiseLastOsError();
//Query effective rights for a trustee
BuildTrusteeWithSid(#tr, bsid);
err := GetEffectiveRightsFromAcl(dacl^, tr, Result);
if err<>ERROR_SUCCESS then
raise Exception.CreateFmt('Cannot calculate effective rights: error %d',[err]);
finally
//Documentation says to free some resources this way when we're done with it.
LocalFree(NativeUint(bsid));
LocalFree(NativeUint(secdesc));
end;
finally
CloseHandle(h);
end;
end;
It's used like this:
var rights,test: cardinal;
rights := AclGetEffectiveRights('C:\My\Folder','S-1-1-0');
//List rights you want tested
test := FILE_LIST_DIRECTORY + FILE_ADD_FILE + FILE_ADD_SUBDIRECTORY
+ FILE_READ_EA + FILE_WRITE_EA + FILE_TRAVERSE + FILE_DELETE_CHILD;
Result := (rights and test) = test;
Might not work; gives ACCESS_DENIED on my PC but that's probably because of my complicated domain situation. Anyway that's something for a start.

So.. The above version worked... until Windows Update 1903 hit me and GetEffectiveRightsFromAcl always resulted in an error ERROR_NO_SUCH_DOMAIN (which is obscure since there is no Domain whatsoever involved here).
So I needed to switch to the following procedure:
// ###########################################
// ### translated and extended from https://learn.microsoft.com/de-de/windows/win32/api/aclapi/nf-aclapi-geteffectiverightsfromacla
procedure DisplayAccessMask(Mask : ACCESS_MASK );
begin
{
// This evaluation of the ACCESS_MASK is an example.
// Applications should evaluate the ACCESS_MASK as necessary.
}
if (((Mask and GENERIC_ALL) = GENERIC_ALL)
or ((Mask and FILE_ALL_ACCESS) = FILE_ALL_ACCESS))
then
begin
OutputDebugString( 'Full control');
exit;
end;
if (((Mask and GENERIC_READ) = GENERIC_READ)
or ((Mask and FILE_GENERIC_READ) = FILE_GENERIC_READ))
then
OutputDebugString( 'Read');
if (((Mask and GENERIC_WRITE) = GENERIC_WRITE)
or ((Mask and FILE_GENERIC_WRITE) = FILE_GENERIC_WRITE))
then
OutputDebugString('Write');
if (((Mask and GENERIC_EXECUTE) = GENERIC_EXECUTE)
or ((Mask and FILE_GENERIC_EXECUTE) = FILE_GENERIC_EXECUTE))
then
OutputDebugString('Execute');
end;
function CheckMask( MASK : ACCESS_MASK; refMask : ACCESS_MASK ) : boolean;
var msk : ACCESS_MASK;
begin
msk := 0;
if (((Mask and GENERIC_READ) = GENERIC_READ)
or ((Mask and FILE_GENERIC_READ) = FILE_GENERIC_READ))
then
msk := msk or FILE_GENERIC_READ;
if (((Mask and GENERIC_WRITE) = GENERIC_WRITE)
or ((Mask and FILE_GENERIC_WRITE) = FILE_GENERIC_WRITE))
then
msk := msk or FILE_GENERIC_WRITE;
if (((Mask and GENERIC_EXECUTE) = GENERIC_EXECUTE)
or ((Mask and FILE_GENERIC_EXECUTE) = FILE_GENERIC_EXECUTE))
then
msk := msk or FILE_GENERIC_EXECUTE;
Result := (msk and refMask) = refMask;
end;
function GetAccess(hAuthzClient :AUTHZ_CLIENT_CONTEXT_HANDLE; psd : PSECURITY_DESCRIPTOR) : BOOL;
var AccessRequest : AUTHZ_ACCESS_REQUEST;
AccessReply : AUTHZ_ACCESS_REPLY;
buffer : Array[0..1023] of Byte;
begin
FillChar(AccessRequest, sizeof(AccessRequest), 0);
FillChar(AccessReply, sizeof(AccessReply), 0);
FillChar(buffer, sizeof(buffer), 0);
AccessRequest.DesiredAccess := MAXIMUM_ALLOWED;
AccessRequest.PrincipalSelfSid := nil;
AccessRequest.ObjectTypeList := nil;
AccessRequest.ObjectTypeListLength := 0;
AccessRequest.OptionalArguments := nil;
AccessReply.ResultListLength := 1;
AccessReply.GrantedAccessMask := PACCESS_MASK( LongWord(#Buffer[0]));
AccessReply.Error := PDWORD( LongWord( AccessReply.GrantedAccessMask ) + sizeof(Access_Mask));
Result := AuthzAccessCheck( 0,
hAuthzClient,
#AccessRequest,
0,
psd,
nil,
0,
#AccessReply,
nil);
if Result then
begin
DisplayAccessMask( AccessReply.GrantedAccessMask^ );
Result := CheckMask( AccessReply.GrantedAccessMask^, FILE_GENERIC_WRITE or FILE_GENERIC_READ );
end
else
RaiseLastOSError;
end;
function ConvertStringSidToSid(StringSid: PWideChar; var Sid: PSID): boolean; stdcall; external advapi32 name 'ConvertStringSidToSidW';
function ConvertNameToBinarySid(pAccountName : PCHAR): PSID ;
var pDomainName : PChar;
dwDomainNameSize : DWord;
aSID : PSID;
dwSIDSIZE : DWORD;
sidType : SID_NAME_USE;
begin
pDomainName := nil;
dwDomainNameSize := 0;
aSID := nil;
LookupAccountName( nil, pAccountName, aSID, dwSIDSIZE, pDomainName, dwDomainNameSize, sidType);
aSid := Pointer( LocalAlloc( LPTR, dwSIDSIZE*sizeof(char)) );
pDomainName := Pointer( LocalAlloc(LPTR, dwDomainNameSize*sizeof(char)) );
if not LookupAccountName( nil, pAccountName, aSID, dwSIDSIZE, pDomainName, dwDomainNameSize, sidType) then
begin
LocalFree( Cardinal(aSID) );
Result := nil;
end
else
begin
Result := aSid;
end;
LocalFree( Cardinal(pDomainName) );
end;
function GetEffectiveRightsForSID(hManager :AUTHZ_RESOURCE_MANAGER_HANDLE;
psd : PSECURITY_DESCRIPTOR;
sid : PChar) : BOOL;
var asid : PSID;
bResult : BOOL;
unusedID : LUID;
hAuthzClientContext : AUTHZ_CLIENT_CONTEXT_HANDLE;
begin
Result := False;
asid := nil;
hAuthzClientContext := 0;
FillChar(unusedID, sizeof(unusedID), 0);
if not ConvertStringSidToSid(sid, asid) then
RaiseLastOsError();
// asid := ConvertNameToBinarySid('rabatscher');
if asid = nil then
RaiseLastOSError;
try
if asid <> nil then
begin
bResult := AuthzInitializeContextFromSid( 0, aSid, hManager, nil, unusedId, nil, #hAuthzClientContext );
try
if bResult then
Result := GetAccess(hAuthzClientContext, psd);
finally
if hAuthzClientContext <> 0 then
AuthzFreeContext(hAuthzClientContext);
end;
end;
finally
if asid <> nil then
LocalFree(LongWord(asid));
end;
end;
function UseAuthzSolution( psd : PSECURITY_DESCRIPTOR; const sid : string = 'S-1-1-0') : boolean;
var hManager : AUTHZ_RESOURCE_MANAGER_HANDLE;
bResult : BOOL;
pSid : PChar;
begin
bResult := AuthzInitializeResourceManager(AUTHZ_RM_FLAG_NO_AUDIT,
nil, nil, nil, nil, #hManager);
if bResult then
begin
pSid := PChar(sid);
bResult := GetEffectiveRightsForSID(hManager, psd, psid);
AuthzFreeResourceManager(hManager);
end;
Result := bResult;
end;
function GetSecurityInfo(handle: THandle; ObjectType: SE_OBJECT_TYPE;
SecurityInfo: SECURITY_INFORMATION; ppsidOwner, ppsidGroup: PPSID; ppDacl, ppSacl: PACL;
var pSecurityDescriptor: PSECURITY_DESCRIPTOR): DWORD; stdcall; external 'ADVAPI32.DLL' name 'GetSecurityInfo'; {use localfree to release ppSecurityDescriptor}
function CheckDirectoryAccess( path : string ) : boolean;
var dw : DWORD;
apacl : PACL;
psd : PSECURITY_DESCRIPTOR;
apSID : PSID;
h : THandle;
begin
try
apSID := nil;
//Open directory
h := CreateFile(PChar(path), GENERIC_READ, FILE_SHARE_READ, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_BACKUP_SEMANTICS, 0);
//we need FILE_FLAG_BACKUP_SEMANTICS to open a directory
if h = INVALID_HANDLE_VALUE then
RaiseLastOsError();
try
//Query access control list for a directory -- the list you see in the properties box
dw := GetSecurityInfo(h, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION or OWNER_SECURITY_INFORMATION or GROUP_SECURITY_INFORMATION,
nil, nil, #apacl, nil, psd);
if dw <> ERROR_SUCCESS then
RaiseLastOSError;
try
Result := UseAuthzSolution(psd);
finally
if apSID <> nil then
LocalFree(NativeUint(apSID));
LocalFree(NativeUint(psd));
end;
finally
CloseHandle(h);
end;
except
on E : Exception do
begin
Result := False;
end;
end;
end;
Note that there are a few changes so the procedure works:
GetSecurityInfo (from the above procedure) needs the parameter DACL_SECURITY_INFORMATION or OWNER_SECURITY_INFORMATION or GROUP_SECURITY_INFORMATION (not only DACL_SECURITY_INFORMATION) otherwise you get an Error 87 in AuthzAccessCheck!
In addition you need to check out the JWA headers from the jedi library.
Hope that helps other people too.

Related

WlanAPI WlanGetNetworkBssList returning invalid data

I'm stuck in my debugging efforts with a call to WlanGetNetworkBssList and would appreciate some pointers. My end objective is to construct a Wifi scanner/profiler tool to help me troubleshoot networking issues on remote sites.
I am using the Windows Native Wifi API (link) and Delphi/Pascal interface found here using Delphi Berlin 10.1 Update 2 under Windows 10 (VCL).
I started with a simple and crude test app (VCL) to get a feel for the API and ran into a problem calling WlanGetNetworkBssList so I created a small console app focused on that problem. The issue is that it works in a console app running in a command prompt but not in my VCL test app. The functions are pretty much copy-paste equivalent and stepping through the code side-by-side shows that the data is identical except for the return data from WlanGetNetworkBssList call (pWlanBssList)
Question: Since the call is to an external DLL what steps can I do to further debug this and understand the difference between the VCL and the console app.
Note: The WlanGetNetworkBssList has two modes of operation where an SSID can be supplied to get the BSSID (MAC of the access point) for that specific SSID. By passing NULL instead of an SSID the API will return the BSSIDs of all APs. Passing NULL works on both the VLC and console app. What breaks is when a specific SSID is requested. After verification, the SSID data structure is identical in both apps but the data buffer returned is invalid with the VCL app. How can this be?
Console app:
program CWifiScan;
{$APPTYPE CONSOLE}
uses
Windows,
System.SysUtils,
nduWlanAPI,
nduWlanTypes;
const
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_ADHOC_PROFILES = $00000001;
var
hWlan: THandle;
guid : TGUID;
dwSupportedVersion: DWORD = 0;
dwClientVersion: DWORD = 1;
i,j : integer;
pInterfaceInfo : Pndu_WLAN_INTERFACE_INFO;
pInterfaceList : Pndu_WLAN_INTERFACE_INFO_LIST;
pAvailableNetworkList : Pndu_WLAN_AVAILABLE_NETWORK_LIST;
procedure GetBSSIDList(clientHandle : THandle;
interfaceGUID : TGUID;
pSSID : Pndu_DOT11_SSID = nil;
SSID_Type : Tndu_DOT11_BSS_Type = dot11_BSS_type_any;
SecurityEnabled : BOOL = True);
var
//to check if interface is connected
pData : Pndu_WLAN_INTERFACE_STATE;
pdwDataSize : DWORD;
isConnected : Boolean;
//to get list of BSSids from available APs
pWlanBssList : Pndu_WLAN_BSS_LIST;
items : integer;
itemIndex : integer;
SSID : string;
MAC : string;
begin
//check if interface is connected
isConnected := False;
if WlanQueryInterface(clientHandle,
#interfaceGUID,
wlan_intf_opcode_interface_state,
nil,
#pdwDataSize,
#pData,
nil) = ERROR_SUCCESS then
begin
isConnected := (pData^ = Tndu_WLAN_INTERFACE_STATE.wlan_interface_state_connected);
end;
//get the list of BSSids for the provided interface
if isConnected then
begin
if WlanGetNetworkBssList(clientHandle,
#interfaceGUID,
pSSID,
SSID_Type,
SecurityEnabled,
nil,
#pWlanBssList) = ERROR_SUCCESS then
begin
items := pWlanBssList^.dwNumberOfItems;
for itemIndex := 0 to items - 1 do
begin
SSID := String(PAnsiChar(#pWlanBssList^.wlanBssEntries[itemIndex].dot11Ssid.ucSSID));
MAC := Format('%.2x:%.2x:%.2x:%.2x:%.2x:%.2x', [
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[0],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[1],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[2],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[3],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[4],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[5]]);
Writeln('');
Writeln('SSID: ................ '+SSID);
Writeln('Physical Address: .... '+MAC);
end; {for itemIndex}
Writeln(#10+#13+'Done.');
end; {WlanGetNetworkBssList succeeds}
end; {isConnected}
end;
begin
hWlan := 0;
if WlanOpenHandle(2, nil,#dwSupportedVersion, #hWlan)= ERROR_SUCCESS then
begin
if WlanEnumInterfaces(hWlan, nil, #pInterfaceList) = ERROR_SUCCESS then
begin
try
for i := 0 to pInterfaceList^.dwNumberOfItems-1 do
begin
Writeln('Wifi Adapter - '+GUIDToString( pInterfaceList^.InterfaceInfo[i].InterfaceGuid ) );
Writeln('Scanning: .... '+pInterfaceList^.InterfaceInfo[i].strInterfaceDescription);
guid := pInterfaceList^.InterfaceInfo[i].InterfaceGuid;
//Get all BSSids for this interface
GetBSSIDList(hWlan, guid);
if WlanGetAvailableNetworkList(hWlan,
#guid,
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_ADHOC_PROFILES,
nil,
pAvailableNetworkList) = ERROR_SUCCESS then
begin
try
for j := 0 to pAvailableNetworkList^.dwNumberOfItems - 1 do
begin
//Get BSSid for this specific SSID
GetBSSIDList(hWlan,
guid,
#pAvailableNetworkList^.Network[j].dot11Ssid,
pAvailableNetworkList^.Network[j].dot11BssType,
pAvailableNetworkList^.Network[j].bSecurityEnabled);
end;
finally
if pAvailableNetworkList<>nil then
WlanFreeMemory(pAvailableNetworkList);
end;
end;
end;
finally
if pInterfaceList<>nil then
WlanFreeMemory(pInterfaceList);
end;
end;
WlanCloseHandle(hWlan, nil);
readln;
end;
end.
The relevant parts of the VCL app are:
uses
... nduWlanAPI, nduWlanTypes, nduWinDot11;
function TForm1.GetBSSID(clientHandle: THandle;
interfaceGuid: TGUID;
pSSID: Pndu_DOT11_SSID = nil;
SSID_Type : Tndu_DOT11_BSS_TYPE = dot11_BSS_type_any;
SecurityEnabled: boolean = true): string;
var
//used to determin if the interface is connected
pData : Pndu_WLAN_INTERFACE_STATE;
isConnected : boolean;
//used to extract a list of BSSIDs for a given interface
pWlanBssList : Pndu_WLAN_BSS_LIST;
lastError : DWORD;
pdwDataSize : DWORD;
items,
itemIndex: Integer;
begin
pData := nil;
pdwDataSize := 0;
isConnected := False;
//check if the interface is connected
lastError := WlanQueryInterface(clientHandle,
#interfaceGuid,
wlan_intf_opcode_interface_state,
nil,
#pdwDataSize,
#pData,
nil);
if (lastError = ERROR_SUCCESS) then
begin
//isConnected := (Tndu_WLAN_INTERFACE_STATE(pData^.isState) = Tndu_WLAN_INTERFACE_STATE.wlan_interface_state_connected);
isConnected := (pData^ = Tndu_WLAN_INTERFACE_STATE.wlan_interface_state_connected);
end
else
DisplayError('Error in WlanQueryInterface() function', lastError);
if isConnected then
begin
pWlanBssList := nil;
lastError := WlanGetNetworkBssList(clientHandle,
#interfaceGuid,
pSSID,
SSID_Type,
SecurityEnabled,
nil,
#pWlanBssList);
try
if (lastError = ERROR_SUCCESS) then
begin
items := pWlanBssList^.dwNumberOfItems;
for itemIndex := 0 to items-1 do
begin
Result := (Format('%.2x:%.2x:%.2x:%.2x:%.2x:%.2x', [
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[0],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[1],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[2],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[3],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[4],
pWlanBssList^.wlanBssEntries[itemIndex].dot11Bssid[5]]));
end;
end
else
DisplayError('Error in the WlanGetNetworkBssList() function call', lastError);
finally
if pData<>nil then
WlanFreeMemory(pData);
if pWlanBssList<>nil then
WlanFreeMemory(pWlanBssList);
end;
end;
end;
Which is called as follows:
function TForm1.ScanWifi(): THandle;
const
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_ADHOC_PROFILES = $00000001;
var
hClient : THandle;
dwVersion : DWORD;
lastError : DWORD;
pInterface : Pndu_WLAN_INTERFACE_INFO_LIST;
i : Integer;
j : Integer;
pAvailableNetworkList: Pndu_WLAN_AVAILABLE_NETWORK_LIST;
interfaceGuid : TGUID;
BSSID : string;
begin
lastError:=WlanOpenHandle(NDU_WLAN_API_VERSION, nil, #dwVersion, #hClient);
if lastError<> ERROR_SUCCESS then
begin
//DisplayError('Error in the WlanOpenHandle() function call', lastError);
Result := 0;
Exit;
end;
//L(Format('Requested WLAN interface version [%d], negotiated version [%d]', [NDU_WLAN_API_VERSION, dwVersion]));
Result := hClient;
try
lastError:=WlanEnumInterfaces(hClient, nil, #pInterface);
try
if lastError<> ERROR_SUCCESS then
begin
//DisplayError('Errorin the WlanEnumInterfaces() function call', lastError);
Exit;
end;
for i := 0 to pInterface^.dwNumberOfItems - 1 do
begin
interfaceGuid:= pInterface^.InterfaceInfo[i].InterfaceGuid;
lastError:=WlanGetAvailableNetworkList(hClient,
#interfaceGuid,
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_ADHOC_PROFILES,
nil,
pAvailableNetworkList);
try
if lastError<> ERROR_SUCCESS then
begin
//DisplayError('Error WlanGetAvailableNetworkList', lastError);
Exit;
end
else
begin
for j := 0 to pAvailableNetworkList^.dwNumberOfItems - 1 do
Begin
BSSID := GetBssid(hClient,
interfaceGuid,
#pAvailableNetworkList^.Network[j].dot11Ssid,
pAvailableNetworkList^.Network[j].dot11BssType,
pAvailableNetworkList^.Network[j].bSecurityEnabled
);
//FAPList.AddOrSetValue(BSSID,J);
end;
end;
finally
if pAvailableNetworkList <> nil then
WlanFreeMemory(pAvailableNetworkList);
end;
end;
finally
if pInterface <> nil then
WlanFreeMemory(pInterface);
end;
finally
WlanCloseHandle(FhClient, nil);
end;
end;
Comparing the data between the two apps the only difference is the result (pWlanBssList) as seen here (left=console, right=VCL):
Looks like compiler bug in boolean conversion, problem is following line in VCL code
SecurityEnabled: boolean = true
you need to change it to
SecurityEnabled: bool = true

Omnikey 5421 can't read SLE4442

I have contact SLE4442 smart card (2W) and Omnikey 5421 smart card reader.
My problem is with connection to card. WinSCard's method SCardConnect returns error SCARD_W_UNRESPONSIVE_CARD 0x80100066. What is interesting this problem not exist with Omnikey 5321 (predecessor 5421). I post code that I used to test read of data (Delphi):
function GetResponseFromCard(const FCardHandle: Integer; const APdu:
string): string;
var
RetVar : cardinal;
SBuf : string;
SLen : cardinal;
RBuf : string;
RLen : cardinal;
Ppci : Pointer;
begin
SBuf := APdu;
RBuf := StringOfChar(#0,260);
Ppci := #SCARD_PCI_T0;
SLen := Length(APdu);
RLen := Length(RBuf);
RetVar := SCardTransmit(FCardHandle, Ppci, Pointer(SBuf), SLen, nil, Pointer(RBuf), #RLen);
if RetVar = SCARD_S_SUCCESS then begin
Result := Copy(RBuf,1,RLen);
end else begin
Result := IntToHex(RetVar, 8);
end;
end;
procedure TestSLE4442;
var
FContext: Cardinal;
PContext: Pointer;
Res: LongInt;
ReaderListStr: string;
ReaderListSize: integer;
v: array[0..10] of string;
SelectedReader: PChar;
phCard: Integer;
pdwActiveProtocol: Integer;
ReadedData: string;
begin
FContext := 0;
PContext := #FContext;
Res := SCardEstablishContext(SCARD_SCOPE_USER, nil, nil, PContext);
if Res = SCARD_S_SUCCESS then begin
Res := SCardListReaders(FContext, nil, nil, ReaderListSize);
if Res = SCARD_S_SUCCESS then begin
SetLength(ReaderListStr, ReaderListSize);
Res := SCardListReaders(FContext, nil, Pointer(ReaderListStr), ReaderListSize);
SortOutSubstrings(ReaderListStr,v,[#0]);
SelectedReader := PChar(v[0]);
ShowMessage('SelectedReader: '+ SelectedReader);
if Res = SCARD_S_SUCCESS then begin
Res := SCardConnect(FContext, SelectedReader, SCARD_SHARE_EXCLUSIVE, SCARD_PROTOCOL_T0, phCard, #pdwActiveProtocol);
if Res = SCARD_S_SUCCESS then begin
ReadedData := GetResponseFromCard(phCard, TStringHelper.Hex2Bin('FFB0000000'));
ShowMessage('Data: '+ ReadedData);
end;
end;
end;
SCardReleaseContext(FContext);
end;
end;
Have someone met with this problem? I also asked HID but without answer for now.
Thanks for help. I heard and I tired Synchronous Omnikey API. Vendor answered me that 2WBP is not supported in new 5421. Maybe there is chance using SCARD_SHARE_DIRECT because using this I'm able to connect to SLE4442 using 5421. But this is hard to tell what I can do more without vendor support.

CreateFileMapping fails with hFile other than INVALID_HANDLE_VALUE

update - solved and answered, offending lines have been commented out
Brief description
I am having a problem linking CreateFile with CreateFileMapping even though I use (GENERIC_WRITE or GENERIC_WRITE) for CreateFile, with PAGE_READWRITE for CreateFileMapping
Detailed description
I am using CreateFileMapping to share memory between proceses.
the actual mechanics of that is working fine, assuming I don't map to a physical file and instead use INVALID_HANDLE_VALUE for the first parameter to CreateFileMapping. This is fine, however what I'd like to achieve is for the main process that creates this map to use a disk based file, and flush it to the drive, periodically, and when it shuts down, so the data is automatically saved.
Code follows...
(when i run this, i get "Error 5 : Access is Denied" as the ShowMessage)
const MaximumMapSize = 256 * 1024;
var virtualMemoryPath : string = '';
function getFileHandle(mapname : string; maxSize : dword) :THandle;
var diskfilename : string;
lpFileName: PChar;
dwDesiredAccess: DWORD;
dwShareMode: DWORD;
lpSecurityAttributes: PSecurityAttributes;
dwCreationDisposition : dword;
dwFlagsAndAttributes: DWORD;
hTemplateFile : THandle ;
temp : pointer;
begin
Result := INVALID_HANDLE_VALUE;
if (maxSize <= MaximumMapSize) and (Length(virtualMemoryPath) > 0) then
begin
diskfilename := virtualMemoryPath+mapname+'.bin';
if FileExists(diskfilename) then
Sysutils.DeleteFile(diskfilename);
lpFileName := PChar(diskfilename);
//dwDesiredAccess := GENERIC_WRITE or GENERIC_WRITE;//<<<wrong
dwDesiredAccess := GENERIC_READ or GENERIC_WRITE;
dwShareMode := 0;//FILE_SHARE_READ or FILE_SHARE_WRITE;//<<wrong
lpSecurityAttributes := nil;
dwCreationDisposition := CREATE_ALWAYS;
dwFlagsAndAttributes := 0;
hTemplateFile := 0 ;
Result := CreateFile(
lpFileName,
dwDesiredAccess,
dwShareMode,
lpSecurityAttributes,
dwCreationDisposition,
dwFlagsAndAttributes,
hTemplateFile);
if (Result <> INVALID_HANDLE_VALUE) then
begin
GetMem(temp,maxsize);
ZeroMemory(temp,maxsize);
FileWrite(result,temp^,maxSize);
FreeMem (temp,maxsize);
FlushFileBuffers(result);
SetFilePointer(result,0,0,FILE_BEGIN);
end;
end;
end;
function createMap (mapname : string ; maxSize: dword; var hFile: THandle) : THandle;
var
lpFileMappingAttributes: PSecurityAttributes;
flProtect : DWORD;
dwMaximumSizeHigh : DWORD;
dwMaximumSizeLow : DWORD;
lpName : PChar;
LastError : dword;
begin
Result := INVALID_HANDLE_VALUE;
if (maxSize > MaximumMapSize) then
exit;
// create a disk the file or return INVALID_HANDLE_VALUE if settings have not defined a path to folder
hFile := getFileHandle(mapname,maxSize);
lpFileMappingAttributes := nil;
flProtect := PAGE_READWRITE;
dwMaximumSizeHigh := 0;
dwMaximumSizeLow := maxSize;
lpName := PChar(mapname);
Result := CreateFileMapping(
hfile,
lpFileMappingAttributes,
flProtect,
dwMaximumSizeHigh,
dwMaximumSizeLow,
lpName);
if (Result = 0) then
begin
if (not (hFile = INVALID_HANDLE_VALUE) ) then
CloseHandle(hFile);
hFile := 0;
LastError := GetLastError();
ShowMessage( Format('Error %d : %s',[LastError,SysErrorMessage(LastError)]) );
end
else
begin
ShowMessage(Format('Returing handle %d',[result]));
if (hFile = INVALID_HANDLE_VALUE) then
hFile := 0;
end;
end;
dwDesiredAccess := GENERIC_WRITE or GENERIC_WRITE;
dwShareMode := FILE_SHARE_READ or FILE_SHARE_WRITE;
This cannot work. You'll need read and write access. Also, creating a view requires the operating system to be sure that it is the only one that can write to the file. There's no mechanism to detect another process writing to the file, other than through a view, and ensure that such a write is visible in memory in a timely and synchronous manner. Particularly the synchronous update is impossible to implement with multiple threads accessing the view.
Read sharing is similarly unwise, the operating system provides no guarantee at which time it updates the file from the view. The only guarantee is that it will be updated when all views are closed. Which also means that an orderly shutdown of Windows is required, you cannot make this reliable against mishaps like a power loss. Again, reads need to be done through the view, not the file. The only appropriate choice is no sharing.
The mistake is here:
GENERIC_WRITE or GENERIC_WRITE
You meant:
GENERIC_READ or GENERIC_WRITE
Fix that and your file mapping can be created.
I agree with Hans that you should not be sharing the file handle.

Using WinInet to identify total file size before downloading it

I got the source below from a third-party site explaining how to download a file from the internet using WinInet. I'm not too familiar with API, and I took at look at the WinInet unit but did not see any API calls like what I need.
What I'm doing is adding the ability to report back progress of downloading a file. This procedure I've already wrapped inside of a TThread and everything works fine. However, just one missing piece: Finding the total size of the source file before downloading.
See below where I have a comment //HOW TO GET TOTAL SIZE? This is where I need to find out what is the total size of the file BEFORE I begin downloading it. How do I go about doing this? Because this code seems to not know the size of the file until it's done being downloaded - and that makes this addition irrelevant.
procedure TInetThread.Execute;
const
BufferSize = 1024;
var
hSession, hURL: HInternet;
Buffer: array[1..BufferSize] of Byte;
BufferLen: DWORD;
f: File;
S: Bool;
D: Integer;
T: Integer;
procedure DoWork(const Amt: Integer);
begin
if assigned(FOnWork) then
FOnWork(Self, FSource, FDest, Amt, T);
end;
begin
S:= False;
try
try
if not DirectoryExists(ExtractFilePath(FDest)) then begin
ForceDirectories(ExtractFilePath(FDest));
end;
hSession:= InternetOpen(PChar(FAppName), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
try
hURL:= InternetOpenURL(hSession, PChar(FSource), nil, 0, 0, 0);
try
AssignFile(f, FDest);
Rewrite(f, 1);
T:= 0; //HOW TO GET TOTAL SIZE?
D:= 0;
DoWork(D);
repeat
InternetReadFile(hURL, #Buffer, SizeOf(Buffer), BufferLen);
BlockWrite(f, Buffer, BufferLen);
D:= D + BufferLen;
DoWork(D);
until BufferLen = 0;
CloseFile(f);
S:= True;
finally
InternetCloseHandle(hURL);
end
finally
InternetCloseHandle(hSession);
end;
except
on e: exception do begin
S:= False;
end;
end;
finally
if assigned(FOnComplete) then
FOnComplete(Self, FSource, FDest, S);
end;
end;
You can use the HEAD method and check the Content-Length to retrieve the file size of a remote file
Check these two Methods
WinInet
If you want execute a HEAD method you must use the HttpOpenRequest, HttpSendRequest and HttpQueryInfo WinInet functions .
uses
SysUtils,
Windows,
WinInet;
function GetWinInetError(ErrorCode:Cardinal): string;
const
winetdll = 'wininet.dll';
var
Len: Integer;
Buffer: PChar;
begin
Len := FormatMessage(
FORMAT_MESSAGE_FROM_HMODULE or FORMAT_MESSAGE_FROM_SYSTEM or
FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_IGNORE_INSERTS or FORMAT_MESSAGE_ARGUMENT_ARRAY,
Pointer(GetModuleHandle(winetdll)), ErrorCode, 0, #Buffer, SizeOf(Buffer), nil);
try
while (Len > 0) and {$IFDEF UNICODE}(CharInSet(Buffer[Len - 1], [#0..#32, '.'])) {$ELSE}(Buffer[Len - 1] in [#0..#32, '.']) {$ENDIF} do Dec(Len);
SetString(Result, Buffer, Len);
finally
LocalFree(HLOCAL(Buffer));
end;
end;
procedure ParseURL(const lpszUrl: string; var Host, Resource: string);
var
lpszScheme : array[0..INTERNET_MAX_SCHEME_LENGTH - 1] of Char;
lpszHostName : array[0..INTERNET_MAX_HOST_NAME_LENGTH - 1] of Char;
lpszUserName : array[0..INTERNET_MAX_USER_NAME_LENGTH - 1] of Char;
lpszPassword : array[0..INTERNET_MAX_PASSWORD_LENGTH - 1] of Char;
lpszUrlPath : array[0..INTERNET_MAX_PATH_LENGTH - 1] of Char;
lpszExtraInfo : array[0..1024 - 1] of Char;
lpUrlComponents : TURLComponents;
begin
ZeroMemory(#lpszScheme, SizeOf(lpszScheme));
ZeroMemory(#lpszHostName, SizeOf(lpszHostName));
ZeroMemory(#lpszUserName, SizeOf(lpszUserName));
ZeroMemory(#lpszPassword, SizeOf(lpszPassword));
ZeroMemory(#lpszUrlPath, SizeOf(lpszUrlPath));
ZeroMemory(#lpszExtraInfo, SizeOf(lpszExtraInfo));
ZeroMemory(#lpUrlComponents, SizeOf(TURLComponents));
lpUrlComponents.dwStructSize := SizeOf(TURLComponents);
lpUrlComponents.lpszScheme := lpszScheme;
lpUrlComponents.dwSchemeLength := SizeOf(lpszScheme);
lpUrlComponents.lpszHostName := lpszHostName;
lpUrlComponents.dwHostNameLength := SizeOf(lpszHostName);
lpUrlComponents.lpszUserName := lpszUserName;
lpUrlComponents.dwUserNameLength := SizeOf(lpszUserName);
lpUrlComponents.lpszPassword := lpszPassword;
lpUrlComponents.dwPasswordLength := SizeOf(lpszPassword);
lpUrlComponents.lpszUrlPath := lpszUrlPath;
lpUrlComponents.dwUrlPathLength := SizeOf(lpszUrlPath);
lpUrlComponents.lpszExtraInfo := lpszExtraInfo;
lpUrlComponents.dwExtraInfoLength := SizeOf(lpszExtraInfo);
InternetCrackUrl(PChar(lpszUrl), Length(lpszUrl), ICU_DECODE or ICU_ESCAPE, lpUrlComponents);
Host := lpszHostName;
Resource := lpszUrlPath;
end;
function GetRemoteFileSize(const Url : string): Integer;
const
sUserAgent = 'Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101';
var
hInet : HINTERNET;
hConnect : HINTERNET;
hRequest : HINTERNET;
lpdwBufferLength: DWORD;
lpdwReserved : DWORD;
ServerName: string;
Resource: string;
ErrorCode : Cardinal;
begin
ParseURL(Url,ServerName,Resource);
Result:=0;
hInet := InternetOpen(PChar(sUserAgent), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
if hInet=nil then
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('InternetOpen Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
try
hConnect := InternetConnect(hInet, PChar(ServerName), INTERNET_DEFAULT_HTTP_PORT, nil, nil, INTERNET_SERVICE_HTTP, 0, 0);
if hConnect=nil then
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('InternetConnect Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
try
hRequest := HttpOpenRequest(hConnect, PChar('HEAD'), PChar(Resource), nil, nil, nil, 0, 0);
if hRequest<>nil then
begin
try
lpdwBufferLength:=SizeOf(Result);
lpdwReserved :=0;
if not HttpSendRequest(hRequest, nil, 0, nil, 0) then
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
if not HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH or HTTP_QUERY_FLAG_NUMBER, #Result, lpdwBufferLength, lpdwReserved) then
begin
Result:=0;
ErrorCode:=GetLastError;
raise Exception.Create(Format('HttpQueryInfo Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
finally
InternetCloseHandle(hRequest);
end;
end
else
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
finally
InternetCloseHandle(hConnect);
end;
finally
InternetCloseHandle(hInet);
end;
end;
Indy
Also check this code using indy.
function GetRemoteFilesize(const Url :string) : Integer;
var
Http: TIdHTTP;
begin
Http := TIdHTTP.Create(nil);
try
Http.Head(Url);
result:= Http.Response.ContentLength;
finally
Http.Free;
end;
end;
Answering the question of how to get a download size with WinInet. This is out of one of my file downloaders that is based on WinInet.
This is the method I use to get the download size:
function TWebDownloader.GetContentLength(URLHandle: HINTERNET): Int64;
// returns the expected download size. Returns -1 if one not provided
var
SBuffer: Array[1..20] of char;
SBufferSize: Integer;
srv: integer;
begin
srv := 0;
SBufferSize := 20;
if HttpQueryInfo(URLHandle, HTTP_QUERY_CONTENT_LENGTH, #SBuffer, SBufferSize, srv) then
Result := StrToFloat(String(SBuffer))
else
Result := -1;
end;
Use of this method requires an open request handle, and does NOT require reading any of the data:
URLHandle := HttpOpenRequest(ConnectHandle, 'GET', Pchar(sitepath), nil,
nil, nil, INTERNET_FLAG_NO_CACHE_WRITE, 0);
...
DownloadSize := GetContentLength(URLHandle);
HTH
after fixing the types it looks better like this:
function GetContentLength(URLHandle:HINTERNET):Int64;
// returns the expected download size. Returns -1 if one not provided
var
SBufferSize, srv:Cardinal;
begin
srv:=0;
SBufferSize:=20;
if Not HttpQueryInfo(URLHandle, HTTP_QUERY_CONTENT_LENGTH or HTTP_QUERY_FLAG_NUMBER, {#SBuffer} #Result, SBufferSize, srv) then Result:=-1;
end;
to call it:
{get the file handle}
hURL:=InternetOpenURL(hSession, PChar(URL), nil, 0, 0, 0);
if hURL=Nil then
begin
InternetCloseHandle(hSession);
ShowMessage('The link is incorrect!');
exit;
end;
{get the file size}
filesize:=GetContentLength(hURL);

Delphi - How to get list of USB removable hard drives and memory sticks?

In my application (Delphi), I need to list all the USB storage devices. These can be either flash memory sticks or external storage drives.
There is a Jvcl component JvDriveCombo, and it has the DriveType property - the problem is if I select DriveType := Fixed then in addition to the external drive, it also lists the internal drives (C:\, D:\ etc). However, I only want to list the external drives.
I believe there is DeviceIoControl function (I saw it on MSDN) but I have no idea of how to use it.
I wonder if anyone can help me with the proper way / code to list USB storage devices?
Thanks.
EDIT:
I just found some sample code and am posting it here:
uses .... jwawinbase, JwaWinIoctl;
procedure TForm1.Button1Click(Sender: TObject);
var
DriveCmdStr: string;
DriveHandle: THandle;
ADriveLetter: string;
hp: STORAGE_HOTPLUG_INFO;
rlen: DWORD;
begin
ADriveLetter := 'H';
DriveCmdStr := Format('\\.\%s:', [ADriveLetter]);
DriveHandle := CreateFile(PChar(DriveCmdStr), GENERIC_READ, FILE_SHARE_WRITE,
nil, OPEN_EXISTING, 0, 0);
if DriveHandle = INVALID_HANDLE_VALUE then
Exit;
DeviceIoControl(DriveHandle, IOCTL_STORAGE_GET_HOTPLUG_INFO, nil, 0, #hp,
SizeOf(hp), #rlen, nil);
CloseHandle(DriveHandle);
if hp.MediaRemovable then
showmessage('media removable');
end;
Now I would like to just know how to enumerate all the drive letters. Which is the most efficient function?
{$MINENUMSIZE 4}
const
IOCTL_STORAGE_QUERY_PROPERTY = $002D1400;
type
STORAGE_QUERY_TYPE = (PropertyStandardQuery = 0, PropertyExistsQuery, PropertyMaskQuery, PropertyQueryMaxDefined);
TStorageQueryType = STORAGE_QUERY_TYPE;
STORAGE_PROPERTY_ID = (StorageDeviceProperty = 0, StorageAdapterProperty);
TStoragePropertyID = STORAGE_PROPERTY_ID;
STORAGE_PROPERTY_QUERY = packed record
PropertyId: STORAGE_PROPERTY_ID;
QueryType: STORAGE_QUERY_TYPE;
AdditionalParameters: array [0..9] of AnsiChar;
end;
TStoragePropertyQuery = STORAGE_PROPERTY_QUERY;
STORAGE_BUS_TYPE = (BusTypeUnknown = 0, BusTypeScsi, BusTypeAtapi, BusTypeAta, BusType1394, BusTypeSsa, BusTypeFibre,
BusTypeUsb, BusTypeRAID, BusTypeiScsi, BusTypeSas, BusTypeSata, BusTypeMaxReserved = $7F);
TStorageBusType = STORAGE_BUS_TYPE;
STORAGE_DEVICE_DESCRIPTOR = packed record
Version: DWORD;
Size: DWORD;
DeviceType: Byte;
DeviceTypeModifier: Byte;
RemovableMedia: Boolean;
CommandQueueing: Boolean;
VendorIdOffset: DWORD;
ProductIdOffset: DWORD;
ProductRevisionOffset: DWORD;
SerialNumberOffset: DWORD;
BusType: STORAGE_BUS_TYPE;
RawPropertiesLength: DWORD;
RawDeviceProperties: array [0..0] of AnsiChar;
end;
TStorageDeviceDescriptor = STORAGE_DEVICE_DESCRIPTOR;
function GetBusType(Drive: AnsiChar): TStorageBusType;
var
H: THandle;
Query: TStoragePropertyQuery;
dwBytesReturned: DWORD;
Buffer: array [0..1023] of Byte;
sdd: TStorageDeviceDescriptor absolute Buffer;
OldMode: UINT;
begin
Result := BusTypeUnknown;
OldMode := SetErrorMode(SEM_FAILCRITICALERRORS);
try
H := CreateFile(PChar(Format('\\.\%s:', [AnsiLowerCase(Drive)])), 0, FILE_SHARE_READ or FILE_SHARE_WRITE, nil,
OPEN_EXISTING, 0, 0);
if H <> INVALID_HANDLE_VALUE then
begin
try
dwBytesReturned := 0;
FillChar(Query, SizeOf(Query), 0);
FillChar(Buffer, SizeOf(Buffer), 0);
sdd.Size := SizeOf(Buffer);
Query.PropertyId := StorageDeviceProperty;
Query.QueryType := PropertyStandardQuery;
if DeviceIoControl(H, IOCTL_STORAGE_QUERY_PROPERTY, #Query, SizeOf(Query), #Buffer, SizeOf(Buffer), dwBytesReturned, nil) then
Result := sdd.BusType;
finally
CloseHandle(H);
end;
end;
finally
SetErrorMode(OldMode);
end;
end;
procedure GetUsbDrives(List: TStrings);
var
DriveBits: set of 0..25;
I: Integer;
Drive: AnsiChar;
begin
List.BeginUpdate;
try
Cardinal(DriveBits) := GetLogicalDrives;
for I := 0 to 25 do
if I in DriveBits then
begin
Drive := Chr(Ord('a') + I);
if GetBusType(Drive) = BusTypeUsb then
List.Add(Drive);
end;
finally
List.EndUpdate;
end;
end;
You can access this information using WMI. If you use this SQL you can access information about installed disks.
select * from Win32_diskdrive where size<>NULL
This code retrive information about drives.
procedure TForm1.DoInventario(aWSQL:string; var mmResult:TMemo);
var
Locator:ISWbemLocator;
Services:ISWbemServices;
SObject:ISWbemObject;
ObjSet:ISWbemObjectSet;
Enum:IEnumVariant;
TempObj:OleVariant;
Value:Cardinal;
TS:TStrings;
begin
try
Locator := CoSWbemLocator.Create();
// Conectar con el Servicio de WMI
Services := Locator.ConnectServer(
STR_LOCALHOST, {ordenador local}
STR_CIM2_ROOT, {root}
STR_EMPTY, STR_EMPTY, {usuario y password -en local no son necesarios-}
STR_EMPTY,STR_EMPTY, 0, nil);
// Acceder a los datos
ObjSet := Services.ExecQuery(aWSQL, 'WQL',
wbemFlagReturnImmediately and wbemFlagForwardOnly , nil);
Enum := (ObjSet._NewEnum) as IEnumVariant;
// Hemos encontrado algun objeto?
while (Enum.Next(1, TempObj, Value) = S_OK) do begin
SObject := IUnknown(TempObj) as ISWBemObject;
// encontrado?
if (SObject <> nil) then begin
// Acceder a la propiedad
SObject.Properties_;
// Cargamos las propiedades
TS := TStringList.Create();
try
TS.Add(SObject.GetObjectText_(0));
// lo pasamos al memo
mmResult.Lines.Text := mmResult.Lines.Text + TS.Text;
finally
FreeAndNil(TS);
end;
end;
end;
except
// Recuperar excepciones
end;
end;
You must add ActiveX and WbemScripting_TLB (this must be imported) in your uses.
With this you can access all information of the disks.
To retrive the letter of all disk you can combine (retrieve can do with the same code) the access to the classes Win32_LogicalDiskToPartition and Win32_DiskDrive.
select * from Win32_LogicalDiskToPartition
select * from Win32_DiskDrive
If you search WMI you can find more related codes.
Regards.
I'm not sure if you're just looking to enumerate drive letters? The for-loop below does that, going through all letters, regardless of whether there's a drive for that letter.
Or, if you're looking for a different way to find removable drives, there's a function for that below, too. (Yours may be better...) Surprisingly, on my test, Windows.GetDriveType does NOT consider CD drives as removable. USB drives are flagged as removable, as one would expect.
Function RemovableDrive(Drive: char): Boolean;
begin
Result := (Windows.GetDriveType(PChar(Drive + ':\')) = Windows.Drive_Removable);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Drive: Char;
begin
for Drive := 'A' to 'Z' do
Memo1.Lines.Add('Drive: ' + Drive + ' is ' + BoolToStr(RemovableDrive(Drive), TRUE));
end;

Resources