Date: | April 12, 2017 / year-entry #91 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20170412-00/?p=95946 |
Comments: | 43 |
Summary: | GetFullPathName will tell you. |
As we learned some time ago, each drive has its own current directory for backward-compatibility with DOS 1.0 (which predates the concept of directories). This is simulated internally by magic environment variables. But those magic environment variables are internal and are therefore not contractual. What is the supported way of finding out the current directory for a non-current drive? One approach I've seen is to perform this sequence of operations: GetCurrentDirectory(ARRAYSIZE(originalDirectory), originalDirectory); SetCurrentDirectory(TEXT("X:")); GetCurrentDirectory(ARRAYSIZE(currentDirectoryOnX), currentDirectoryOnX); SetCurrentDirectory(originalDirectory); One problem with this approach is that it may not be able to return to the original directory if the original directory was deleted or is otherwise not settable. Another problem with this approach is that it applies a global solution to a local problem: The current directory is a process-wide resource, so if there is another thread that is performing an operation that depends on the current directory, you just messed up that thread.
The supported way to get the current directory for
a non-current drive is to call
TCHAR buf[BUFFERSIZE]; TCHAR *file; GetFullPathName(TEXT("X:"), BUFFERSIZE, buf, &file);
Assuming the call succeeds, the result in the buffer
is the current directory for drive |
Comments (43)
Comments are closed. |
This is a really nice tip because it is very unobvious how to do this.
It is just a shame that the documentation for this function doesn’t make it clearer that it can do this. When the first parameter is named file name and there is nothing stating that you can put in a volume and you will get out the current directory, it takes a lot of reading between the lines to notice this.
The funny thing is that to me it was totally obvious that “X:” refers to the current directory on drive X. If you do “copy somefile X:” it goes into the current directory on drive X:. If you do “dir X:” you get the current directory on drive X.
Yeah, but if you are writing that in a program then the API has differences and you will be in that mindset. It is also not as if the Win32 API has a precedence for this in the first place.
Like if you wrote
CopyFileW(srcFile, L”X:”, FALSE);
Would you expect that to succeed? I wouldn’t, I would expect that to fail because you are trying to copy the file to a volume name. (And it does fail with access denied.)
In the copy file documentation the second parameter is given to be the new file name, and nothing in the description hints at you being able to use a path in there.
The same is true with GetFullPathName, the first parameter is file name, and nothing hints at you being able to use a path or volume name (to refer to the current directory) in there in just the description. You have to go through the remarks section to come across the things related to the shares to even get a hint at this.
The parameter to CopyFileW is a file name, and “X:” is not a file name. But “X:Foo” would work. And GetFullPathName works with directories as well as files. It produces the full path. Could be the full path to a file, or the full path to a directory. And “X:” is a directory. The documentation for GetFullPathName needs to be clearer that the first parameter can be either a file or directory.
Would “X:.” (driver letter, colon, period), with the period to explicitly reference the current directory of the drive in question, work?
FWIW, the “Remarks” section of the MSDN documentation for GetFullPathName()[0] explicitly mentions the “X:” case (although it uses “U:”) and implies that it will return the root directory of the specified drive, as opposed to the current directory. Of course, it will return the root directory if the current directory of the drive is the root directory, but that is not explicit, and is (IMHO) not the most straightforward reading of the text.
[0] https://msdn.microsoft.com/en-us/library/windows/desktop/aa364963(v=vs.85).aspx
In that example, U: is a network mapped drive.
So? Do mapped network drives not have a “current directory”?
“The current directory is a process-wide resource.”
Which raises the question of why it is so, by the way. One would expect the current directory to be thread-specific (or if compatibility demands it, thread-specific for threads that ask for it, like with SetThreadLocale()).
The current directory is just another way of saying the working directory, isn’t it? And considering the implications of DLL loading on the active working directory, I suspect that making the working directory thread-global instead of process-global could have unsettling implications when attempting to load DLLs in your program’s working directory.
You shouldn’t be loading DLLs from the working directory anyway, that’s how you get DLL hijack vulnerabilities.
We’ll just fire up the time machine and fix that….
Please don’t, that’s how DirectX / Glide wrappers work.
Performing SetDllDirectory(“”) does this. (See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms686203(v=vs.85).aspx)
@xcomcmdr: This all is about the current directory, not the startup directory. As noted in MSDN in the topic “Dynamic-Link Library Search Order”, the directory from which the application loaded is always searched first.
So you should be able to place your DirectX / Glide wrapper DLLs in the directory of the respective executable.
How do you get the current directory for a non-current drive in a thread-safe manner, since GetFullPathNameW is not thread-safe?
Would it work to use CreateFile2 with FILE_FLAG_BACKUP_SEMANTICS to open “X:” as a directory handle then GetFinalPathNameByHandleW on that handle?
The documentation makes it semi-clear that the reason why the function is not multi-thread safe is because the current directory is not multi-thread safe. You ask for the current directory on drive X: and immediately after the OS fill your buffer but before the function returns, another thread changes the current directory. You now have invalid, out of date information.
(IOW, a non-absolute path in a multi-threaded application is ill-defined.)
I wish that the Win32 API had a way to open a file relative to an open directory handle rather than a process-wide current directory. Linux supports this through openat() and friends. The underlying Windows NT API also supports this feature (see OBJECT_ATTRIBUTES::RootDirectory of NtCreateFile), but Win32 doesn’t give a way to access it.
You can use the directory handle to list the contents of the directory, including the file IDs, and then use OpenFileById. Awkward, though, and there are limitations – you can’t delete a file that way, for example.
FileDispositionInfo can be used to delete. Symlinks seem to be problematic when walking a tree by fileids though.
@skSdnW, if the handle was opened with OpenFileById the file system won’t let you set the delete flag.
Use GetFinalPathNameByHandle to retrieve the path to the directory, then you can open a file relative to that. https://msdn.microsoft.com/en-us/library/windows/desktop/aa364962(v=vs.85).aspx
No it isn’t. Most applications don’t change their current directory after startup. Of course for those that do you are pretty much right.
And most applications that do, do so without knowing it, thanks to an OPENFILENAME. Even OFN_NOCHANGEDIR doesn’t actually prevent this.
I’ve seen a printer driver that decided it would be fine to change the current directory of the process it was loaded in.
I am afraid (based on what is written in the MSDN article) that the hazard of GetFullPathName() (or GetCurrentDir(), even) in a multithreaded process is much more serious than receiving a not-up-to-date response. The docs explain that the current directory data is stored in a not thread safe global variable for the whole process, and an unlucky SetCurrentDir() in a parallel thread could cause data corruption.
I think that paragraph is being overly-cautious. From what I can tell, the calls themselves are thread-safe, but of course it’s hard to use them in a thread-safe manner because anybody can change the current directory at any time. I will double-check and revise the documentation as appropriate.
Thanks
DOS does not “predate the concept of directories”. CP/M didn’t implement directories, but directories-in-a file-system concept was 15 years old before CP/M existed.
Nitpickers’ corner, eh ?
It’s not nitpicking to point out that “X hadn’t been invented” is categorically different from “We didn’t implement X because we didn’t think it was important”.
It seems to me to be nitpicking to interpret “preceded the concept of X” as “X hadn’t been invented yet” when the intended meaning in context was clearly “this particular operating system didn’t have the concept of X”. :-)
Typical calling names just because you’re 15 years late to the party.
Calling names?
During the DOS 1.0 days, directories really weren’t an important feature. The original IBM PC was a floppy-only system with a maximum disk capacity of 320 KB. You’d have at most a few dozen files on a disk, and if you wanted to “organize” them, you’d move some of the files to another floppy. It was the introduction of hard drives (which could hold a whopping 20 MB or so) in DOS 2.0 that made a hierarchical “directory” structure useful for PCs.
“Predates the introduction of directories.”
Am I the only one who wonders why anybody would want to do this?
The concept of “current directory on a drive” is long since deprecated. Why would anyone rely on it?
If you are implementing a component that accepts paths, you don’t get to control what kinds of paths people will pass you.
Every application choses what they accept as a valid path. “Current dir on another drive” is not important to support, most users are not aware of this unexpected feature from the DOS age, it usually makes more sense to interpret it as an invalid path or relative to the root folder, that’s actually what many applications do anyway, as a user you cannot trust that this feature works anyway.
If you are a library component, you probably need to support anything that can be passed to “CreateFile”.
I think I’ve encountered a few command-line tools that don’t handle relative drive-letter paths properly, but not many. It is such a useful feature, regardless of its historical origins, that it is certainly worth supporting whenever possible.
This is actually somewhat useful, due to NT’s futile attempt to prevent the current dir on the current drive from being removed (with a useless handle), a work a round is to change the current drive, remove the folder, and change the current drive back. Now you have an open handle to a non-existing folder, precisely as in w9x.
I wonder what happens if another process removes or renames the directory that is marked as current on drive X: for my process. Will my process now point into the air? Will it follow the renamed/moved path?
“a work a round is to change the current drive, remove the folder, and change the current drive back..”
That is not possible: As soon as you change “the current drive”, you are changing the one and only real current directory of the process. This changes the handle that is used to lock the current directory.
After you removed the previous-current directory, you are unable to change back to that. (The old cwd does no longer exists, so how do you expect the kernel to perform this, and in addition, give the process an “open handle to a non-existing folder” at that point?)
I’m assuming this applies per drive letter, not necessarily per partition, if non-letter mountpoints are used… (They probably don’t have seperate current directories?)