Sunday, February 4, 2024

A User-Space File System for Fun and History

I got my start with computers as a kid with an Atari 800 way back when.  When I'm feeling nostalgic, I still enjoy pulling it out and playing with it.  These days, I'm more likely to pull out an emulator than the real thing, as they're now so precise that it's very difficult to find a difference, and it eliminates all the hassles involved with disks that were slow and may have failed with age.  That means I need tools for manipulating disk images.

So there I was...  I was posting at AtariAge about a tool I wrote to extract the files from a disk image, and another to create an image containing a set of files, and someone remarked that they were surprised that nobody had created a Linux kernel module to mount the disk image files as a file system.  That was a dangerous comment.  Writing a Linux kernel module to support Atari file systems is certainly possible, but would be a lot of work.  But there's another option, which is to write a file system in user space using a package called "FUSE."

I've written a file system using FUSE before, and it's far less complicated than writing one in kernel space.  Besides making debugging vastly simpler, you can also just skip lots of things and the library will take care of it for you.  And with Atari file systems, there are, of course, many modern features that just aren't supported.  And another big advantage of FUSE is that it works on MacOS as well as Linux, so it makes the project available to a wider audience.  (It also runs on the Windows Subsystem for Linux.)

But note that I said Atari file systems, not file system.  Depending on how you count, I found at least seven or perhaps ten different file systems.  (Several are variations, so they can share most of the same code.)  This means the first task is to detect which file system a disk image is using, if any (as many programs just read disk sectors directly and didn't use a file system at all).  

So the first step is to have sanity checking routines to determine if a disk image is consistent for each file system.  Then implement the key file system features for each DOS version.  The key functions that had to be implemented are reading a directory, getting file attributes, reading a file, and writing a file.  Most other features are trivial or unsupported for most file systems, but I implemented as much as possible.  For example, files could be locked, which I interpreted as the write-permission bit for the file owner.  Some file systems did support time stamps on files, but for most, I just copied the time stamp on the disk image file.

But once it was working, it was quite simple to add more features.  So I made files like ".sector361" that would contain the raw bytes of sector 361.  The file doesn't have to exist in the directory, but as long as it's supported by the get attributes, read, and possibly write functions, it will work just fine.  This feature was also very helpful in debugging the file systems, as I could look at a hexdump of raw sectors when something wasn't behaving as expected, or when I had to reverse engineer a file system with incomplete specifications.

Of course, every time I finished a file system or new feature, someone on AtariAge would find yet another for me to look at.  That was really half the fun of it.  I discovered a number of file systems used on the Atari that I hadn't encountered before, and there were some weird ones.  The oddities of those file systems probably deserves its own blog post.

But once I had it pretty solid, there was another wrinkle.  There's a special "Atari Partition Table" format for hard disks that's used with some newer add-ons, including flash card readers that people now use with their old computers.  This presented two major problems.  First, the documentation for the APT images, while detailed, had points that were confusing or ambiguitiies, and included many options that aren't actually used.  This required getting sample images from users, as I didn't use it myself.  Second, this meant having a number of different file systems mounted in subdirectories.  That required refactoring the code, including adding a middle layer to call into.  Sometimes adding an extra layer of indirection lets you do magic.

Unfortunately, this doesn't work for all file systems in an APT partition, as the file system code uses a memory map of the image file, and with some APT options, the disk image is no longer a straight linear stream of bytes.  This has to do with fitting smaller sector sizes into 512-byte sectors when sometimes you care a lot more about efficient code than efficient space.  But since I can export the image of the contents of each partition, even if the bytes have to be scrambled by the read and write functions, that image can then be opened separately by a new instance of my program, creating a new mount point.  A bit awkward, but it works.

So that was a fun project.  While this had no commercial value, it was interesting to look at ancient file systems and see how they did things, as well as to play around with FUSE.

So here is the code if you're interested:

https://github.com/pcrow/atari_8bit_utils/tree/main/atrfs

Now I suppose what's needed next is a wrapper to take the code for a kernel file system, and run it under FUSE.  That would allow you to develop a kernel file system with all the convenience of user-space tools, but you could then build the code as a kernel module once it's working.  That's a project for another day.

No comments:

Post a Comment