24 days of Rust - FUSE filesystems, part 1

Important note: this article is outdated! Go to http://zsiciarz.github.io/24daysofrust/ for a recent version of all of 24 days of Rust articles. The blogpost here is kept as it is for historical reasons.

A traditional filesystem is typically implemented as a kernel module. However, some Unix-like operating systems (Linux, FreeBSD, Mac OS X and a few others) allow for userspace filesystems through a mechanism called FUSE. The canonical FUSE library is written in C and there are some bindings from other languages (Python, Ruby etc.)

The fuse crate is very interesting because it's a rewrite from C to Rust, leveraging many of Rust features unavailable in C. The only binding to libfuse is related to mounting and unmounting the filesystem, the rest is pure Rust.

Small disclaimer: I'm not very well versed in filesystems, in fact this is my first attempt at FUSE. I'm learning a lot while writing this article, hopefully for the best of all of us :-)

JSON filesystem

In this and the following article we'll try to create a read-only FUSE filesystem that represents a JSON object (a key-value map). The keys will map to filenames, while file contents will represent values stored under respective keys.

Let's start with a filesystem that does absolutely nothing, but can be mounted and unmounted. The following example is almost directly taken from rust-fuse sources. This will be our starting point:

extern crate fuse ; use std :: os ; use fuse :: Filesystem ; struct JsonFilesystem ; impl Filesystem for JsonFilesystem { } fn main () { let mountpoint = match os :: args (). as_slice () { [ _ , ref path ] => Path :: new ( path ), _ => { println ! ( "Usage: {} <MOUNTPOINT>" , os :: args ()[ 0 ]); return ; } }; fuse :: mount ( JsonFilesystem , & mountpoint , & amp ;[]); }

Create the mount point (fancy name for an empty directory) and run the program:

$ mkdir /tmp/rust-fuse $ cargo run /tmp/rust-fuse

Note: If you get fusermount: failed to open /etc/fuse.conf: Permission denied errors, you need to add yourself to the fuse group:

$ sudo addgroup <USERNAME> fuse

Log out and log in again to apply changes.

Now try the following in another terminal window:

$ ls /tmp/rust-fuse ls: cannot access /tmp/rust-fuse: Function not implemented

Great! This means our filesystem is mounted, but listing directory contents is not implemented. In fact no action is implemented yet, so let's get to work!

By the way - to unmount your filesystem, stop the Rust program and then execute fusermount -u /tmp/rust-fuse . If you don't unmount, you'll run into errors next time you try cargo run .

Attributes

First of all we need to add a number of imports at the top.

extern crate libc ; extern crate time ; use std :: io :: { FileType , USER_FILE , USER_DIR }; use std :: mem ; use std :: os ; use libc :: { ENOENT , ENOSYS }; use time :: Timespec ; use fuse :: { FileAttr , Filesystem , Request , ReplyAttr , ReplyEntry , ReplyDirectory };

Let's start with the getattr function. FUSE will call it when reading file or directory attributes, such as size, creation time, uid etc.

impl Filesystem for JsonFilesystem { fn getattr ( & mut self , _req : & Request , ino : u64 , reply : ReplyAttr ) { println ! ( "getattr(ino={})" , ino ); reply . error ( ENOSYS ); } }

We still return an ENOSYS status code (this is what causes the Function not implemented error), but we will see in the program output that FUSE called getattr on inode number 1, which is the root directory of our filesystem. OK, now let's flesh out this method:

fn getattr ( & mut self , _req : & Request , ino : u64 , reply : ReplyAttr ) { println ! ( "getattr(ino={})" , ino ); let mut attr : FileAttr = unsafe { mem :: zeroed () }; attr . ino = 1 ; attr . kind = FileType :: Directory ; attr . perm = USER_DIR ; let ttl = Timespec :: new ( 1 , 0 ); if ino == 1 { reply . attr ( & ttl , & attr ); } else { reply . error ( ENOSYS ); } }

The FileAttr struct is somewhat similar to struct stat in C. We initialize it with zeros (just as call to memset in C would do) and later fill only a few selected fields. If you now run stat on the root directory, you'll see the permissions and familiar dates from the 70's (timestamps are 0):

$ stat /tmp/rust-fuse File: ` /tmp/rust-fuse ' Size: 0 Blocks: 0 IO Block: 4096 directory Device: 27h/39d Inode: 1 Links: 0 Access: ( 0755/drwxr-xr-x ) Uid: ( 0/ root ) Gid: ( 0/ root ) Access: 1970-01-01 01:00:00.000000000 +0100 Modify: 1970-01-01 01:00:00.000000000 +0100 Change: 1970-01-01 01:00:00.000000000 +0100

Directory listing

Now it's time to make ls work in our filesystem (for some definition of work...).

fn readdir ( & mut self , _req : & Request , ino : u64 , fh : u64 , offset : u64 , mut reply : ReplyDirectory ) { println ! ( "readdir(ino={}, fh={}, offset={})" , ino , fh , offset ); reply . error ( ENOSYS ); }

We start with a dummy readdir method as before, let's see if it gets called when ls -ing:

$ cargo run /tmp/rust-fuse getattr ( ino = 1 ) readdir ( ino = 1, fh = 0, offset = 0 )

Yay! ls still displays an error, but that's because we wanted it to (remember, ENOSYS ). Now we can actually implement the directory listing functionality.

fn readdir ( & mut self , _req : & Request , ino : u64 , fh : u64 , offset : u64 , mut reply : ReplyDirectory ) { println ! ( "readdir(ino={}, fh={}, offset={})" , ino , fh , offset ); if ino == 1 { if offset == 0 { reply . add ( 1 , 0 , FileType :: Directory , & PosixPath :: new ( "." )); reply . add ( 1 , 1 , FileType :: Directory , & PosixPath :: new ( ".." )); } reply . ok (); } else { reply . error ( ENOENT ); } }

As with getattr we check only the first inode, otherwise return a File not found error ( ENOENT ). The offset == 0 check is necessary, otherwise readdir will loop infinitely. So what happens if we run ls now?

$ ls -la /tmp/rust-fuse total 4 drwxr-xr-x 0 root root 0 Jan 1 1970 . drwxrwxrwt 10 root root 4096 Dec 10 00:38 ..

Hooray! We've implemented an empty directory :-)

To be continued...

That was a lot for today, time to take a short break. See you tomorrow in part 2!

See also

Code examples in this article were built with rustc 0.13.0-nightly.

Photo by Razor512 and shared under the Creative Commons Attribution-NonCommercial 2.0 Generic License. See https://www.flickr.com/photos/razor512/3422485686

Napisane 15 grudnia 2014