there's a bunch of dumb bullshit going on here. I started writing this last night and it's kinda long.
which, very rare, I can talk about because it happened in the open.
Swift carries some amount of cross-platform compatibility. There is a reimplementation of Foundation called swift-corelibs-foundation, which reimplements substantial bits of the framework with the same name — so that your macOS code that uses import Foundation has at least a good subset of API it can rely on on Windows and Linux.
Poor compnerd, the Windows champion, and I (but vastly mostly him) spent an absurd amount of days trying to figure out how the heck we could adapt the way you refer to a file on a Mac to Windows paths.
Mac uses UNIX paths, in general, but, for reasons†, you use Foundation's URL type to refer to a file on disk. In general, save for a bunch of edge cases that only apply on Apple OSes✶, for a path /a/b/c/d/e.txt, you have a URL pointing to file:///a/b/c/d/e.txt (where the host portion is canonically empty and generally ignored).
However, sometimes you need to use POSIX API or old Foundation API. The old API, some of which don't have a 1:1 replacement‡, take a string that is the full path to the file to operate on. Now, however, here's a problem: strings in Foundation/ObjC/Swift are collections of Unicode code points, but paths are byte buffers. Now, my friends, guess: if the system wants or gives you a file path, what encoding is it in?
The answer is obviously 'whatever the OS/filesystem decides it is', which means it's basically kind of whatever? And of course if you use one of the autodetect̨-encoding methods and it messes up, your path will be mangled and everyone will be sad. So what the system says is that you are not allowed to just grab random bytes and shove them in a String constructor — you need to ask the OS to turn what it calls the file system representation (the byte buffer with the path) into a string in a special way, so it can pick a platform-dependent OS path encoding that can represent all possible path byte buffers (usually converting from and to UTF-8, which is the underlying storage of Swift strings and URLs).
So, long story short:
- the system wants you to keep your file reference as a URL…
- … which it can either convert to a path byte buffer (what it calls the 'file system representation')…
- … or from/to a string built in a special way, which is guaranteed to always be convertible to a path byte buffer.
This means having three transforms:
- From a path byte buffer to a string and vice versa (via the
FileManagerclass, which is responsible for basic disk I/O: itsfileSystemRepresentation(withPath:)andstring(withFileSystemRepresentation:length:)methods do that.) - From a path byte buffer to a URL (
URL(fileURLWithFileSystemRepresentation:isDirectory:relativeTo:)) and back (withUnsafeFileSystemRepresentation(_:).) - From a string path to a URL (via the
URL(fileURLWithPath:)constructor) and vice versa.
OK, so, what is that last one? Of course, people were Clever. You can see from the file:///a/b/c/d/e.txt example that the macOS path is just the URL's path portion. So, URL's path property — the one that would return the path portion of a non-file URL, for example the /millenomi in https://cohost.org/millenomi — to return a string for file URLs that is in the right format and encoding to map to the above. Easy, no?
Except when it came to Windows paths, that doesn't hold anymore :(
compnerd came up with this setup, which AFAICT works out nicely:
- The 'file system representation', aka the path byte buffer, is just whatever the local API accept — in this case, the wchar_t array Windows path management functions use. (This is already a departure — macOS paths IIRC use UTF-8 in a specific normal form here.) This is your usual
C:\A\B\C\D\E.txtstring, in the format WinAPI expects. - The string is an appropriately mapped version of the above, with encoding/decoding choices that ensure it can encode anything Windows throws at it.
- The URL is of the form
file:///c:/a/b/c/d/e.txt. If you ask for its path, it returns a string in a different format:c:/a/b/c/d/e.txt.
It turns out — as the original post shows — that a lot of Windows API are agnostic to \ vs /; the canonical one they'll return is \, but they also accept /, which is not a valid filename character anyway. compnerd made sure that all string-accepting API that were cross-platform could operate independently of \ or /-ness. This preserves almost every transform back-and-forth more or less transparently, and allows the framework to return the full, regular string if you go down direct translation paths (e.g., from URL to byte array).
And of course, he had to make sure this setup also worked similarly, and correctly, with every variant of Windows path listed above. It was A Ton of Terrible Work.
† Ironically, URLs are used because that's what Core Foundation introduced to abstract paths between 'Classic' Mac OS's HFS-style paths, and modern Mac OS X's UNIX-style ones. There was a toolkit called Carbon which included Core Foundation that you could use to make apps that could run on both OSes.
‡ Mostly, these were API coming from NeXTSTEP's Foundation implementation. It took a long time, after the decision was made to use URLs to refer to documents with the introduction of CF, to propagate that decision to the higher-level API.
✶ Things I'm eliding here:
- file reference URLs, that lie about their paths and are not supported outside Apple OSes;
- how extended paths could work (basically, we would detect when they're needed and produce an appropriate file system rep, but we would be liberal wrt accepting strings with or without the marker and would still use file URLs of the form
file:///c:/…even if they're long or use Unicode); - you may notice that getting a URL path to a file on the Mac yields a '/…' and on Windows yields a string without the '/' — there's a bunch of special casing IIRC.
- I am not touching the new methods that use the new
System-moduleFilePathparameters, which essentially are just fancy wrappers over file system representations (aka, path byte buffers), other than that they're probably better to work with, they should work on Windows similarly, and you may be using them inadvertently without knowing because there's a new constructor calledURL(filePath:…)that takes them and they are expressible via string literals, so you may do:
let root = URL(filePath: "/")
and it works the way you'd expect even if you don't know you're taking a Module Detour ™.