Windows: add fallback if canonicalize fails - #161951
Conversation
Get the NT path then search for a drive that links to a prefix of it.
|
r? @clarfonthey rustbot has assigned @clarfonthey. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
This will probably need a libs discussion. But it isn't urgent. |
| ) | ||
| } | ||
|
|
||
| /// Fallback in case `get_path_canonical` fails. |
There was a problem hiding this comment.
So, I know it's going to be a pain, but it would be worth adding some of the extra context that you shared on Zulip (+ more if you've got it) on when exactly this might fail. Knowing that it generally is a driver issue for the specific drive (whether it's physical hardware or virtual) is helpful here, and it would also be helpful to know why the normal method can fail while this would still succeed.
There was a problem hiding this comment.
I've now added a brief explanation
| |buf, sz| unsafe { c::GetFinalPathNameByHandleW(handle, buf, sz, c::VOLUME_NAME_NT) }, | ||
| |nt_path| { | ||
| // SAFETY: `GetLogicalDrives` only returns information. | ||
| let drives = unsafe { c::GetLogicalDrives() }; |
There was a problem hiding this comment.
Since we have safe externs now, how much of a pain would it be to edit the script to add safety to bindings.txt and maybe mark this function as safe?
If the answer is too much, that's okay, just thinking it might be helpful.
There was a problem hiding this comment.
We don't have much control of the script. What I can do is write a safe wrapper function that. That could also handle the point below of wanting a nice iterator.
There was a problem hiding this comment.
I've add a couple of safe wrappers that hopefully address your over comments too. The api:: module is like the c:: except it contains safe(r) wrappers instead of direct calls to the Windows API.
| let result = unsafe { | ||
| c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH) | ||
| } as usize; |
There was a problem hiding this comment.
Assuming the safety invariant here is just buffer validity, but worth adding an explicit comment.
| let drives = unsafe { c::GetLogicalDrives() }; | ||
| let mut buf = [0_u16; c::MAX_PATH as usize]; | ||
| for letter in b'A'..=b'Z' { | ||
| let enabled = drives >> (letter - b'A') & 1; | ||
| if enabled == 1 { |
There was a problem hiding this comment.
Small preference for making a wrapper around GetLogicalDrives() that returns an impl Iterator here, instead of inlining this logic into this function, since it is kind-of sandwiched between code with this unrelated buffer too.
Something like:
fn get_drive_letters() -> impl Iterator<Item = u8> {
let drives = unsafe { c::GetLogicalDrives() };
(b'A'..=b'Z').filter(|letter| drives >> (letter - b'A') & 1 == 1)
}There was a problem hiding this comment.
Note: doing an impl Iterator also allows for an option where you make the original iterator 0..26 and then add b'A' to the result, if you so wish. The possibilities are endless, but it will probably be one of these two options.
| let result = unsafe { | ||
| c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH) | ||
| } as usize; | ||
| if result < buf.len() { |
There was a problem hiding this comment.
If I'm reading this correctly, wouldn't this always be true?
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-querydosdevicew#return-value
Says here that the return value is zero when it fails.
There was a problem hiding this comment.
Oh oops. I was confusing it with another API. I mean, it should usually succeed but there are definitely failure modes.
| c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH) | ||
| } as usize; | ||
| if result < buf.len() { | ||
| let drive_path = buf[..result].trim_suffix(&[0, 0]); |
There was a problem hiding this comment.
This trim_suffix is confusing me. I get one trailing zero because C, but why two?
There was a problem hiding this comment.
Ah that's because QueryDosDeviceW can return a list of null-terminated paths and the list is also null terminated.
The function fills this buffer with one or more null-terminated strings. The final null-terminated string is followed by an additional NULL.
We give it a device name so it'll only return one path but for consistency it still returns such a list.
This attempts a partial workaround for issues such as: #59392, #79449, #59107, #54875, #52440, #52377, #48249, #74327, #55812
This may require a bit of explanation depending on how familiar you are with Windows paths, I'll try to keep it brief. The short version is that the above issues are cases where third party devices don't integrate with the system sufficiently so Windows isn't aware of the canonical drive for a particular path, causing our
canonicalizefunction to fail. This PR works around it by manually search for the drive letter that corresponds to the root of the path. This only works in cases where there is a drive letter assigned but that is the majority of cases. It won't work when the device is only mounted to a directory in another filesystem or isn't mounted at all.To explain the implementation of this PR you should be aware that Windows on Windows NT is more like WINE on Linux then many people realise. You have a kernel (NT) and then you have an implementation of the Win32 APIs on top (this is why
kernel32.dllis nothing to do with the real kernel, it's like an implementation of Win95's kernel API on top of another OS). Admittedly the boundaries have become fuzzier over the years but there still remains a clear distinction between the Win32 API and the NT kernel API in many places.Paths are one place where this distinction is made clear. You have the familiar Win32 paths like
C:\path\to\filethat date back to the time of DOS. And then you have the low-level NT kernel paths that look like\Device\HarddiskVolume6\path\to\file(which aren't really meant to be user-visible). To bridge the gap, the NT namespace has a special??directory containing mappings (i.e. symlinks) from drives likeC:to paths like\Device\HarddiskVolume6. In that way translating between Win32 and NT paths is made simpler as you can replaceC:with\??\C:and it'll get resolved to the right path and vice versa (the actual translation from win32 to NT is more complicated but I've already spent too many words on this).So back to canonicalisation. Resolving the canonical NT path should always succeed. The problem comes when mapping that to a Win32 drive path. When the drive is managed by the system then when resolving paths it knows which drive to pick. However, if the drive mapping is added manually then it doesn't.
So the way to workaround this is to manually look at the drive mappings and see which one is the root of the NT path we have.