One of the challenges in implementing a cross-platform file system driver such as Cascade File System is dealing with the many differences, small and large, between how Windows, Linux, and Macintosh file systems work. Some of these differences are well-known and obvious, but there are a lot of other interesting differences underneath the covers, especially when you get down into the file system driver kernel interfaces.
Let’s start with the most obvious one: case sensitivity. Linux has a case sensitive file system. Windows has a case-preserving but case-insensitive file system. Or, at least it looks like Windows does! But in reality Windows supports both. Check out the documentation for the NtCreateFile API, the native NT API that the Win32 API CreateFile maps to. By setting or not setting OBJ_CASE_INSENSITIVE, you can select which type of name lookup you prefer. It’s really up to the individual file system to decide how to interpret all these flags, though. Some Windows file systems, like the original FAT, aren’t even case-preserving.
The Macintosh is now Unix-based as of OSX, but its HFS file system has traditionally been case-insensitive and case-preserving, just like Windows. More recently, Apple now allows HFS to be formatted either way, as either case-sensitive or case-insensitive, but the default remains case-insensitive.
The issue of case sensitivity brings up another issue: internationalization. Windows, Linux, and Macintosh all support Unicode in paths; Windows encodes them as UTF-16 in all of its native APIs, whereas Linux and Macintosh use UTF-8. A problem: it’s possible for two non-identical Unicode strings to correspond to the same sequence of characters. That is, certain characters can be legally encoded in more than one way. Macintosh therefore requires all UTF-8 filenames to be stored in a canonicalized format and therefore will prevent you from creating two files in the same directory with the same name but different character encodings. Windows and Linux do not; this can cause interoperability problems moving data back and forth between the two.
There are several challenges in doing case-insensitive string comparisons in a Unicode-capable file system. NTFS on Windows adopts the following approach: two strings are compared by converting them both to uppercase first, then comparing them for exact equality. The conversion to uppercase is done using a 64K-entry, 128KB table stored on the volume and filled in when the partition is formatted; this ensures that the comparisons do not break (which could cause two files’ names to start colliding) when new characters are added to Unicode and someone upgrades their OS.
Windows uses backslashes as path separators, while Linux and Macintosh use forward slashes. Most of the Win32 APIs allow you to specify forward slashes and will do the conversion for you, but once you get into the NT kernel and the other low-level APIs, backslashes are mandatory.
This in turn means that the set of legal filenames differs between the operating systems. On Linux, for example, you can create a file whose name contains a backslash, while on Windows you cannot. Linux is very permissive about the legal character set, but Windows has a lot of extra restrictions. A filename cannot end with a space or a period; there are a number of reserved names like COM1 and NUL; and several other non-path-separator characters like <, >, :, “, and |, are reserved.
Windows has drive letters, Linux and Macintosh have mount points. Actually, inside the NT kernel and inside kernel drivers, there is really no such thing as a drive letter. A “drive letter” is nothing other than a symbolic link in the NT kernel namespace, e.g., from \DosDevices\X: to \Device\cfs. When you call CreateFile with a path x:\foo.txt, the driver owning the \Device\cfs namespace simply sees a request for \foo.txt. But for practical purposes, this is still important. A Windows path needs to be interpreted differently depending on whether it’s a drive letter path or a UNC path. A Windows file system can be ripped away from applications with files open by removing the symbolic link, whereas a Unix file system cannot be unmounted if files are still open.
The Windows cache manager holds files open. When you close the last handle to a file, from the file system driver’s point of view, the file may still be open. This makes it very difficult to unload a Windows file system driver without a reboot. Unmounting it, i.e., removing the drive letter symbolic link, is easy, but until memory pressure forces the Windows cache manager to flush its cached mappings, those cached mappings may stay around indefinitely.
Permissions are very different. Linux has the standard Unix “UID, GID, mode bits” permissions model, and Macintosh inherits this also. Both have added ACL-based permissions later, but their use is often not considered very mainstream. Windows, on the other hand, is thoroughly ACL-based. Every file in a Windows file system has a security descriptor that includes the ACL. The permissions are far more elaborate than just “read, write, execute”; there are over a dozen types of permissions that you can be granted or denied.
Other file attributes are also different. Windows has a standard list of file attribute bits like “archive”, “hidden”, and “system” that go back to the DOS era. There is no equivalent to these on Unix. All of the systems support a more generic “extended attribute” system, however.
Linux doesn’t have multiple data streams per file. One of the defining properties of Unix, going back to its very beginnings, is that a file is just an array of bytes. Windows, however, allows a file to have multiple “data streams”, while Macintosh supports a similar “resource fork” feature. Apple now discourages the use of resource forks, but multiple data streams continue to be an important feature on Windows in some cases. For example, Internet Explorer attaches an alternate data stream to each file you download to indicate where you downloaded it from. When you later try to run an app that was downloaded from an untrusted zone, you will get a warning asking you whether you really want to do that.
Windows has limited symbolic link support. Windows has “reparse points”, which are like symbolic links for directories only, with some other caveats; but they are supported poorly by many applications. Vista adds something closer to real Unix symbolic links, though again with some limitations.
NtCreateFile() on Windows throws in the kitchen sink. This API has a lot of flexibility that doesn’t exist in the Unix open() system call. For better or worse, just about everything goes through it. For example, there is no equivalent to mkdir() on Windows. Instead, NtCreateFile takes a flag to request that you want to create a directory rather than a file in the event that the path lookup fails. It also supports a number of other random features, like delete-on-close files.
The Windows delete and rename model is different. You wouldn’t know this from the Win32 APIs, but in order to delete or rename a file in Windows, you first have to open it! Once you’ve opened it can you call NtSetInformationFile with InformationClass of FileDispositionInformation or FileRenameInformation. Setting FileDispositionInformation doesn’t even delete the file; it merely enables delete-on-close for the file, and the delete-on-close request could very well be cancelled later.
File sharing restrictions and locking are different. Unix generally avoids the idea of restricting what can be done with a file just because someone else is using it. Having a file open doesn’t prevent it from being unlinked, and two people can open the same file for writing. On Windows, all of this is true in theory — you can request whatever sharing mode you want when you open a file — but in practice, most applications use restrictive sharing modes, preventing two apps from using the same file at the same time. Inside a single file, we also have byte range locking. Windows uses mandatory locking: if someone else has the bytes locked, an attempt to modify those bytes with WriteFile() will fail (but this is not enforced for memory-mapped files!). Unix uses only advisory locking and makes no effort to error-check read() or write() calls; it assumes that the application will be responsible and won’t touch data it hasn’t first locked.
This list of differences could go on and on. It’s a challenge to make sure that CFS supports all of the important file system semantics correctly across the platforms, especially because the revision control systems CFS builds on often have different semantics of their own that don’t quite match the standard file systems.