I am recursing a folder structure in Windows, getting the unique filesystem identifiers for each file and folder. For each folder, I am using the CreateFile()
and DeviceIoControl()
WinAPI functions to get access to the folder ID, as per an answer to this question:
However, for some paths (not all), despite being able to browse them in Explorer, and even System.IO.Path.Exists()
asserts they exist and are accessible, DeviceIoControl()
returns error code 2, ERROR_FILE_NOT_FOUND
.
The last test seems to confirm that it is not a matter of folder permissions, but it is interesting that the error is specifically about a file not found, not a path not found (would be error code 3, ERROR_PATH_NOT_FOUND
). I have confirmed that System.IO.File.Exists()
correctly rejects all folders, even if DeviceIoControl()
does access the path and return meaningful values.
What specifically is DeviceIoControl()
sensitive to, and why is it tripping over on valid paths?
Windows 10
Visual Studio Express 2022
.NET 8.0
WinAPI calls:
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
Public Class WinAPI
Public Shared GENERIC_READ As Integer = &H80000000
Public Shared FILE_FLAG_BACKUP_SEMANTICS As Integer = &H2000000
Public Shared OPEN_EXISTING As Integer = &H3
<StructLayout(LayoutKind.Sequential)>
Public Structure FILE_OBJECTID_BUFFER
Public Structure Union
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
Public BirthVolumeID() As Byte
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
Public BirthObjectId As Byte()
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
Public DomainID As Byte()
End Structure
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
Public ObjectID As Byte()
Public BirthInfo As Union
'<MarshalAs(UnmanagedType.ByValArray, SizeConst:=48)>
'Public ExtendedInfo As Byte()
End Structure
Public Structure FILETIME
Public dwLowDateTime As UInteger
Public dwHighDateTime As UInteger
End Structure
<StructLayout(LayoutKind.Sequential)>
Public Structure BY_HANDLE_FILE_INFORMATION
Public FileAttributes As UInteger
Public CreationTime As FILETIME
Public LastAccessTime As FILETIME
Public LastWriteTime As FILETIME
Public VolumeSerialNumber As UInteger
Public FileSizeHigh As UInteger
Public FileSizeLow As UInteger
Public NumberOfLinks As UInteger
Public FileIndexHigh As UInteger
Public FileIndexLow As UInteger
End Structure
<DllImport("kernel32.dll", SetLastError:=True)>
Public Shared Function DeviceIoControl(
hDevice As SafeFileHandle,
dwIoControlCode As UInteger,
lpInBuffer As IntPtr,
nInBufferSize As UInteger,
lpOutBuffer As IntPtr,
nOutBufferSize As Integer,
ByRef lpBytesReturned As UInteger,
lpOverlapped As IntPtr
) As Boolean
End Function
<DllImport("kernel32.dll", SetLastError:=True)>
Public Shared Function CreateFile(
fileName As String,
dwDesiredAccess As Integer,
dwShareMode As System.IO.FileShare,
securityAttrs_MustBeZero As IntPtr,
dwCreationDisposition As System.IO.FileMode,
dwFlagsAndAttributes As Integer,
hTemplateFile_MustBeZero As IntPtr
) As SafeFileHandle
End Function
<DllImport("kernel32.dll", SetLastError:=True)>
Public Shared Function GetFileInformationByHandle(
hFile As IntPtr,
ByRef lpFileInformation As BY_HANDLE_FILE_INFORMATION) As Boolean
End Function
End Class
Implementation functions:
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
Public Class FileAccess
Shared FSCTL_GET_OBJECT_ID As UInteger = &H9009C
Public Shared Function GetFileId(path As String) As UInt128
Using fs = IO.File.Open(
path,
IO.FileMode.OpenOrCreate,
IO.FileAccess.ReadWrite,
IO.FileShare.ReadWrite)
Dim info As WinAPI.BY_HANDLE_FILE_INFORMATION
WinAPI.GetFileInformationByHandle(fs.Handle, info)
Dim fileID As UInt128 = info.FileIndexHigh
fileID <<= 32
fileID = fileID Or info.FileIndexLow
Return fileID
'Return String.Format("{0:x}", ((info.FileIndexHigh << 32) Or info.FileIndexLow))
End Using
End Function
Public Shared Function GetFolderID(path As String) As UInt128
Dim result As UInt128
Dim buffer As WinAPI.FILE_OBJECTID_BUFFER = GetFolderIdBuffer(path)
result = BitConverter.GetUint128(buffer.ObjectID)
Return result
End Function
Private Shared Function GetFolderIdBuffer(path As String) As WinAPI.FILE_OBJECTID_BUFFER
Using hFile As SafeFileHandle = WinAPI.CreateFile(
path,
WinAPI.GENERIC_READ, IO.FileShare.Read,
IntPtr.Zero,
WinAPI.OPEN_EXISTING,
WinAPI.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero
)
If hFile Is Nothing Or hFile.IsInvalid Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error())
Dim buffer As New WinAPI.FILE_OBJECTID_BUFFER
Dim nOutBufferSize As Integer = Marshal.SizeOf(buffer)
Dim lpOutBuffer As IntPtr = Marshal.AllocHGlobal(nOutBufferSize)
Dim lpBytesReturned As UInteger
Dim result As Boolean =
WinAPI.DeviceIoControl(
hFile, FSCTL_GET_OBJECT_ID,
IntPtr.Zero, 0,
lpOutBuffer, nOutBufferSize,
lpBytesReturned,
IntPtr.Zero
)
If Not result Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error())
Dim type As Type = GetType(WinAPI.FILE_OBJECTID_BUFFER)
buffer = Marshal.PtrToStructure(lpOutBuffer, type)
Marshal.FreeHGlobal(lpOutBuffer)
Return buffer
End Using
End Function
End Class
Consuming method:
Dim folderID As UInt128 = FileAccess.GetFolderID(path)
From the FSCTL_GET_OBJECT_ID
documentation
If there is no object identifier associated with the specified handle, none is created and an error is returned.
It doesn't say which error, but ERROR_FILE_NOT_FOUND is a likely match.
Next it says
To create an object identifier, use
FSCTL_SET_OBJECT_ID
. To retrieve an existing object identifier or generate one if there is no existing object identifier in one step, useFSCTL_CREATE_OR_GET_OBJECT_ID
.
which provides you with a solution.
NB: There is no single semantic for DeviceIoControl
, as it provides you a way to dispatch to a large number of different functions in different drivers (your case is probably a call into the NTFS filesystem driver), identified by IOCTL codes, and each of these different codes has different preconditions and postconditions.