Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装

# はじめに

## 3行で
この記事は、RustのFUSEインターフェースである `fuse-rs` を用いてFUSEを使ったファイルシステムの実装に挑戦し、得られた知見などを記録したものです。
複数回に分けて、徐々に関数を実装していきます。今回は、あらかじめ用意されたファイルをreadできるようになる所までいきます。
作成したファイルシステムの最新版は [こちら](https://github.com/narumatt/sqlitefs)に置いてあります。

# 記事一覧
**Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装**
[Rustで学ぶFUSE (2) ファイルの書き込み](https://qiita.com/narumatt/items/5b15dbabfeef32bfaf5c)
[Rustで学ぶFUSE (3) ファイル作成と削除](https://qiita.com/narumatt/items/ad2d9227710dbeb600be)
[Rustで学ぶFUSE (4) ディレクトリ作成と削除](https://qiita.com/narumatt/items/bec85dd5218a85a089b8)
[Rustで学ぶFUSE (5) 名前の変更と移動](https://qiita.com/narumatt/items/293a1c1f30b1aa9a8270)

## 概要

**Filesystem in Userspace** (FUSE) はLinuxのユーザ空間でファイルシステムを実現する仕組みです。

一般的にファイルシステムを作るというと、カーネルモジュールを作成しなければならないのでいろいろと苦労が多いですが、FUSEを使えば比較的楽に実装できます。
また、HDDなどの実デバイスに直接読み書きするだけでなく、ネットワークストレージを利用した仮想的なファイルシステムを作るのにも都合がよいです。

そんな訳で、FUSEを使った[SSH接続先をマウントできるソフトウェア](https://github.com/libfuse/sshfs) や [AWS S3をマウントできるソフトウェア](https://github.com/s3fs-fuse/s3fs-fuse)といった「読み書きできる何かをファイルシステムとしてマウント出来るようにするソフトウェア」があれこれと存在します。
上記のようなソフトウェアの代表例である `sshfs` や `s3fs` は使った事のある人もいるのではないでしょうか。

元々はLinuxの一部でしたが、MacOSやFreeBSDでも使用できます。最近ではWindowsのWSL2でも使えるようになるようです。WSLの導入の手間が要るとはいえ、WindowsでもFUSEが使えるのは嬉しいですね。
ちなみにWindowsには、Fuseに似た仮想ファイルシステムである [Dokan](https://github.com/dokan-dev/dokany) もあります。

ただし、カーネルモジュールを作るより楽とはいえ、FUSEを使ったソフトウェアを作成するのは大変です。
ある程度ファイルシステムの知識は必要ですし、何か調べようとしてもドキュメントが少なく、チュートリアルを見てもほどほどの所で終わってしまい、「あとはサンプルとsshfsの実装などを見てくれ！」とコードの海に投げ出されます。

そこで、各所の情報をまとめつつ、自分で0からファイルシステムを実装して気をつける点などを見つけていきます。
ついでにRustも学べればいいな…という欲を出して、実装はRustを使っていきます。

## 参考資料
[Rust FUSE](https://github.com/zargony/fuse-rs) : Rust版Fuseインターフェースのプロジェクト 
[libfuse](https://github.com/libfuse/libfuse) : C版のFuseインターフェースライブラリ 
[osxfuse](https://github.com/osxfuse/fuse) : MacOS向けのFuseモジュール 
[FUSEプロトコルの説明](https://john-millikin.com/the-fuse-protocol) : カーネルモジュール <-> Fuseライブラリ間のプロトコル 
[VFSの説明](https://ja.osdn.net/projects/linuxjf/wiki/vfs.txt) 
[fuse_lowlevel.h(libfuseのヘッダ)](https://github.com/libfuse/libfuse/blob/master/include/fuse_lowlevel.h): lowlevel関数の説明 
[fuse_common.h(libfuseのヘッダ)](https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h) 
[Linuxプログラミングインターフェース(書籍)](https://www.oreilly.co.jp/books/9784873115856/) : システムコールがどう動くべきかは大体ここを見て判断する 
[libfuseのメーリングリストのアーカイブ](https://sourceforge.net/p/fuse/mailman/fuse-devel/) : fuseの使い方についてはここが一番参考になる 
[gcsf](https://github.com/harababurel/gcsf) : fuse-rsを使ったファイルシステムの例 

## 実験環境
プログラムは全て次の環境で実験しています。

- Linux: 5.3.11
- ディストリビューション: Fedora 31
- Rust: 1.39.0
- fuse-rs: 0.3.1

## FUSEの仕組み(概要)

FUSE本体はLinuxカーネルに付属するカーネルモジュールで、大抵のディストリビューションではデフォルトで有効になっています。

FUSEを使ったファイルシステムがマウントされたディレクトリ内に対してシステムコールが呼ばれると、以下のように情報がやりとりされます。

```
システムコール <-> VFS <-> FUSE <-(FUSE ABI)-> FUSEインターフェース <-(FUSE API)-> 自作のファイルシステム <-> デバイスやネットワーク上のストレージ
```

[Wikipediaの図](https://ja.wikipedia.org/wiki/Filesystem_in_Userspace) を見ると分かりやすいです。  
一般的なファイルシステムであればVFSの先に各ファイルシステムのカーネルモジュールがあるのですが、FUSEは受け取った情報をユーザ空間に横流ししてくれます。

## FUSEインターフェース

FUSEはデバイス `/dev/fuse` を持ち、ここを通じてユーザ空間とやりとりを行います。
前項の `FUSE <-> FUSEインターフェース` の部分です。

規定のプロトコル(FUSE ABI)を用いて `/dev/fuse` に対してデータを渡したり受け取ったりするのがFUSEインターフェースです。
有名なライブラリとして、C/C++用の [libfuse](https://github.com/libfuse/libfuse) があります。
このlibfuseが大変強力なので、大抵の言語でのFUSEインターフェースはlibfuseのラッパーになっています。

libfuseを使うと、 `open`, `read`, `write` 等の関数を決められた仕様通りに作成して登録するだけで、ファイルシステムとして動作するようになっています。
例えば、 `read(2)` のシステムコールが呼ばれると、最終的に自作のファイルシステムの `read` 関数が呼ばれます。
以下の例は、自作の `read` 関数です。引数や戻り値の仕様はlibfuseによって決められていますが、内部をどう実装するかは自由です。

```c
// read関数を、常にランダムな内容を返すようにした例
int my_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    ssize_t res;
    res = getrandom(buf, size, 0);
    return (int)res;
}
```

登録すべき関数は、 `libfuse` のヘッダーである `fuse.h` 内で定義されている通常のものと、 `fuse_lowlevel.h` 内で定義されている低級なものがあります。
ファイルシステムを作成する場合、どちらの関数群を実装するか選択する必要があります。 
`fuse.h` の方はおおよそシステムコールと1:1で対応しています。 `fuse_lowlevel.h` の方は `FUSE ABI` と1:1になるように作られています。

## fuse-rs
Rustには(ほぼ)独自のFUSEインターフェースの実装 [Rust FUSE(fuse-rs)](https://github.com/zargony/fuse-rs) があります。ありがたいですね。
プロトコルが同じなので、インターフェースの関数(FUSE API)はlibfuseのlowlevel関数と大変似ています。そのため、何か困った時にはlibfuseの情報が流用できたりします。  

現時点(2020/1) の最新版は0.3.1で、2年ぐらい更新されていませんが、次バージョン(0.4.0)が開発中です。
0.3.1と0.4.0では仕様が大きく異なるので注意してください。
また、0.3.1では対応するプロトコルのバージョンが7.8で、最新のものと比較していくつかの機能がありません。

libfuseはマルチスレッドで動作し、並列I/Oに対応していますが、fuse-rsはシングルスレッドのようです。

使用するためには、 `Cargo.toml` に以下のように記述します。

```toml
[dependencies]
fuse = "0.3.1"
```

# データの保存先
今回自分でファイルシステムを実装していく上で、HDDの代わりになるデータの保存先としてsqliteを使用します。ライブラリは [rusqlite](https://github.com/jgallagher/rusqlite) を使用します。
FUSEの実装方法について調べるのがメインなので、こちらについてはざっくりとしか説明しませんが、ご容赦ください。

実装の話が見たい方は [次章までスキップ](#hello) してください。

sqliteは可変長のバイナリデータを持てるので、そこにデータを書き込みます。トランザクションがあるので、ある程度アトミックな操作ができます。DBなので、メタデータの読み書きも割と簡単にできるでしょう。

fuse-rsが扱う整数の大半は `u64` ですが、sqliteはunsignedの64bit intに対応していないので、厳密にやろうとするといろいろと面倒になります。 
とりあえず全部 `u32` にキャストする事にしますが、気になる場合は `i64` にキャストして、大小比較を行うユーザ定義関数をsqlite上に作成したり、`u32` 2個に分割したりしてください。

以下ではDBの構造についてざっくりと説明していきます。

## データベース構造
テーブルはメタデータテーブル(MDT)とディレクトリエントリテーブル(DET)とブロックデータテーブル(BDT)の3つに分けます。
今後拡張ファイル属性が必要になってきた場合、拡張属性データテーブル(XATTRT)を追加します。

以下では各テーブルについて説明していきます。

### メタデータ用テーブル(MDT)
ファイルのinode番号をキーとして検索するとメタデータが返ってくるような、メタデータ用のテーブルを作ります。 
メタデータは一般的なファイルシステムのメタデータと同じような形式です。
fuse-rsが関数の引数で渡してきたり、戻り値として要求したりするメタデータ構造体は以下のように定義されています。

```rust
// fuse::FileAttr
pub struct FileAttr {
    /// inode番号
    pub ino: u64,
    /// ファイルサイズ(バイト単位)
    pub size: u64,
    /// ブロックサイズ *Sparse File に対応する場合、実際に使用しているブロック数を返す
    pub blocks: u64,
    /// Time of last access. *read(2)実行時に更新される
    pub atime: Timespec,
    /// Time of last modification. *write(2)またはtruncate(2)実行時に更新される
    pub mtime: Timespec,
    /// Time of last change. *メタデータ変更時に更新される。 write(2)またはtruncate(2)でファイル内容が変わるときも更新される
    pub ctime: Timespec,
    /// Time of creation (macOS only)
    pub crtime: Timespec,
    /// ファイル種別 (directory, file, pipe, etc)
    pub kind: FileType,
    /// パーミッション
    pub perm: u16,
    /// ハードリンクされている数
    pub nlink: u32,
    /// User id
    pub uid: u32,
    /// Group id
    pub gid: u32,
    /// Rdev *デバイスファイルの場合、デバイスのメジャー番号とマイナー番号が入る
    pub rdev: u32,
    /// Flags (macOS only, see chflags(2)) *非表示などmac特有の属性が入ります。
    pub flags: u32,
}
```

これに合わせて、以下のようなテーブルを作ります。

|列名 | 型 | 概要|
|---|---|---|
|id|integer primary|ファイルのinode番号 (pkey)|
|size|int|ファイルサイズ|
|atime|text|アクセス時刻|
|atime_nsec|int|アクセス時刻(小数点以下)|
|mtime|text|修正時刻|
|mtime_nsec|int|修正時刻(小数点以下)|
|ctime|text|ステータス変更時刻|
|ctime_nsec|int|ステータス変更時刻(小数点以下)|
|crtime|text|作成時刻(mac用)|
|crtime_nsec|int|作成時刻(小数点以下)|
|kind|int|ファイル種別|
|mode|int|パーミッション(ファイル種別含む)|
|nlink|int|ハードリンク数|
|uid|int|uid|
|gid|int|gid|
|rdev|int|デバイスタイプ|
|flags|int|フラグ(mac用)|

idをinteger primary keyにします。これがinode番号になります。

kindはファイル種別です。
通常ファイル・キャラクターデバイス・ブロックデバイス・FIFO・ソケット・ディレクトリ・シンボリックリンク の7種類があります。  
FUSEでは `stat(2)` と同様に、 `mode` にファイル種別のビットも含まれているので、
ビット操作する必要があります。  
cのlibfuseでは `libc::S_IFMT` (該当ビットのマスク) `libc::S_IFREG` (通常ファイルを示すビット) 等を用いて
`if((mode & S_IFMT) == S_IFREG)` のようにして判別する事ができます。  
fuse-rsの場合はメタデータを返す時はenumで定義されたファイル種別を使い、ビット操作はライブラリ側で処理してくれるので、
実際のビットがどうなっているかを気にするケースはあまりありませんが、 `mknod` の引数で `mode` が生の値で渡ってくるので、 `mknod` を実装する場合は気をつける必要があります。

### ファイルデータ用テーブル(BDT)
ファイルのinode番号とファイル内のブロック番号を指定するとデータが返ってくるような、ブロックデータテーブルを作成します。 
ブロックデータテーブル(BDT)のblobにデータを格納します。
BDTはファイルのinode番号, 何番目のブロックか、の列を持ちます。具体的には以下のようになります。

|列名 | 型 | 概要|
|---|---|---|
|file_id|int|ファイルのinode番号 (pkey)(foreign key)|
|block_num|int|データのブロック番号(1始まり)(pkey)|
|data|blob|データ(4kByte単位とする)|

外部キー `foreign key (file_id) references metadata(id) on delete cascade` を指定する事で、ファイルのメタデータが消えたらデータも削除されるようにします。

「あるファイルのあるブロック」は一意なので、主キーとして `(file_id, block_num)` を指定します。

### ディレクトリ構造用テーブル(DET)
ディレクトリ構造を表現する方法は、以下の2つの候補があります。

1. オブジェクトストレージのように、各ファイルがフルパスを記憶していて、文字列操作で各ディレクトリの情報を得る方法
1. 一般的なファイルシステムのように、ディレクトリエントリを作る方法

今回はfuse-rsの関数とも相性のいい後者のディレクトリエントリ方式で行います。
ディレクトリのinode番号を指定すると、ディレクトリ内の全てのエントリ(ファイル名、ファイルタイプ、inode番号のセット)を返すようなテーブルを作成します。

必要なのは以下のデータです。

|列名 | 型 | 概要|
|---|---|---|
|parent_id|int|親ディレクトリのinode番号 (pkey)(foreign key)|
|child_id|int|子ファイル/子ディレクトリのinode番号 (foreign key)|
|file_type|int|子のファイルタイプ|
|name|text|子のファイル/ディレクトリ名 (pkey)|

# Hello
## 概要
第一段階として、fuse-rsに付属する、サンプルプログラムの `HelloFS` と同じ機能を実装します。
`HelloFS` は以下の機能があります。

1. ファイルシステムはリードオンリー
1. ルート直下に `hello.txt` というファイルがあり、 `"Hello World!\n"` という文字列が書き込まれている

`fuse::Filesystem` トレイトが用意されているので、必要な関数を実装していきます。  
`HelloFS` の機能を実現するのに必要なのは以下の4つの関数です。

```rust
use fuse::{
    Filesystem,
    ReplyEntry,
    ReplyAttr,
    ReplyData,
    ReplyDirectory,
    Request
};
impl Filesystem for SqliteFs {
    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry){
        ...
    }
    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
        ...
    }
    fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, _size: u32, reply: ReplyData) {
        ...
    }
    fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        ...
    }
}
```

ファイルやディレクトリをopen/closeする関数を実装せずにread関数やreaddir関数を実装していますが、`libfuse` や `fuse-rs` は全ての関数にデフォルトの実装があり、今回のようにreadonlyで状態を持たないファイルシステムの場合、自分で実装しなくても動作します。
これらの関数については今後実装する必要が出てきた時に説明します。


## DB関数
データベースを読み書きする関数です。
今回作成した関数は以下になります。

```rust
pub trait DbModule {
    /// ファイルのメタデータを取得する。見つからない場合は0を返す
    fn get_inode(&self, inode: u32) -> Result<DBFileAttr, Error>;

    /// ディレクトリのinode番号を指定して、ディレクトが持つディレクトリエントリを全て取得する
    fn get_dentry(&self, inode: u32) -> Result<Vec<DEntry>, Error>;

    /// 親ディレクトリのinode番号と名前から、ファイルやサブディレクトリのinode番号とメタデータを得る
    /// inodeが存在しない場合、inode番号が0の空のinodeを返す
    fn lookup(&self, parent: u32, name: &str) -> Result<DBFileAttr, Error>;

    /// inode番号とブロック数を指定して、1ブロック分のデータを読み込む
    /// ブロックデータが存在しない場合は、0(NULL)で埋められたブロックを返す
    fn get_data(&self, inode: u32, block: u32, length: u32) -> Result<Vec<u8>, Error>;

    /// DBのブロックサイズとして使っている値を得る
    fn get_db_block_size(&self) -> u32;
}

// メタデータ構造体
pub struct DBFileAttr {
    pub ino: u32,
    pub size: u32,
    pub blocks: u32,
    pub atime: SystemTime,
    pub mtime: SystemTime,
    pub ctime: SystemTime,
    pub crtime: SystemTime,
    pub kind: FileType,
    pub perm: u16,
    pub nlink: u32,
    pub uid: u32,
    pub gid: u32,
    pub rdev: u32,
    pub flags: u32,
}

// ディレクトリエントリ構造体
pub struct DEntry {
    pub parent_ino: u32,
    pub child_ino: u32,
    pub filename: String,
    pub file_type: FileType,
}
```

## ルートディレクトリ
あらゆるディレクトリは `.` (自分自身)と `..` (親ディレクトリ)のエントリを持ちます。
ルートの `..` は自分自身を指すようにします。

何も入っていないファイルシステムでも、初期データとしてルートディレクトリの情報が必要です。FUSEでは、ルートディレクトリのinode番号は1です。
ルートディレクトリは必ず存在する必要があります。

## fuseの関数全般の話
### fuseの関数
ファイルシステムなので、関数は基本的に受け身です。システムコールに応じて呼び出されます。
fuse-rsでは、 `Filesystem` トレイトが定義されているので、必要な関数を適宜実装していきます。

### 引数
どの関数にも `Request` 型の引数 `req` が存在します。
`req.uid()` で実行プロセスのuidが、 `req.gid()` でgidが、 `req.pid()` でpidが取得できます。

### 戻り値
`init` 以外の各関数に戻り値は存在せず、引数の `reply` を操作して、呼び出し元に値を受け渡します。
`ReplyEmpty, ReplyData, ReplyAttr` のように、関数に応じて `reply` の型が決まっています。

`reply.ok()` `reply.error(ENOSYS)` `reply.attr(...)` 等 `reply` の型に応じたメソッドを実行します。

エラーの場合、 `libc::ENOSYS` `libc::ENOENT` のような定数を `reply.error()` の引数に指定します。

## lookup
```rust
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry);
```

引数の `parent` で親ディレクトリのinode番号、 `name` で当該ディレクトリ/ファイルの名前が与えられるので、メタデータを返します。
lookup実行時には `lookup count` をファイルシステム側で用意して、増やしたりしなければなりませんが、今回はreadonlyのファイルシステムなので無視します。
`lookup count` については `unlink` 実装時に説明します。

replyに必要なデータは以下になります。

```
    //正常
    reply.entry(&TTL, &ATTR, &GENERATION);
    エラーの場合
    reply.error(ENOENT);
```

### Replyの引数
`reply.entry()` の3つの引数について説明します。

#### TTL
`time::Timespec` で期間を指定します。
TTLの間はカーネルは再度問い合わせに来ません。

今回は、以下のような `ONE_SEC` という定数を作って返しています。

```
const ONE_SEC: Timespec = Timespec{
    sec: 1,
    nsec: 0
};
```

#### ATTR
対象のメタデータ。 `fuse::FileAttr` を返します。

#### GENERATION
inodeの世代情報を `u64` で返します。削除されたinodeに別のファイルを割り当てた場合、前のファイルと違うファイルである事を示すために、generationに別の値を割り当てます。
ただし、この値をチェックするのは(知られているものでは)nfsしかありません。
今回は常時 `0` に設定します。

[libfuseの説明](https://libfuse.github.io/doxygen/structfuse__entry__param.html#a4c673ec62c76f7d63d326407beb1b463) も参考にしてください。

#### エラー

対象のディレクトリエントリが存在しない場合、 `reply.error(ENOENT)` でエラーを返します。

### 実装
実装は以下のようになります。

```rust
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
    match self.db.lookup(parent as u32, name.to_str().unwrap()) {
        Ok(attr) => {
            reply.entry(&Timespec{sec: 1, nsec: 0}, &attr.get_file_attr() , 0);
        },
        Err(_err) => reply.error(ENOENT)
    };
}
```

## getattr
```rust
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr);
```

引数の `ino` でinode番号が指定されるので、ファイルのメタデータを返します。
メタデータの内容については `lookup` で返す `ATTR` と同じです。

```rust
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
    match self.db.get_inode(ino as u32) {
        Ok(attr) => {
            reply.attr(&ONE_SEC, &attr.get_file_attr());
        },
        Err(_err) => reply.error(ENOENT)
    };
}
```

## read
```rust
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, size: u32, reply: ReplyData);
```

引数の `ino` のinode番号で指定されたファイルを、 `offset` で指定されたバイトから `size` で指定されたバイト分読み込みます。
読み込んだデータは `reply.data(&data)` を実行して返します。

EOFまたはエラーを返す場合を除いて、 `read` 関数は引数の `size` で指定されたサイズのデータを返さないといけません。
例えば、長さ200byteのファイルに対して、4096byteの要求が来ることがあります。
この場合EOFなので、200byte返す事が許されます。また、200byte以上返しても切り捨てられます。
それ以外の場合で要求された長さのデータを用意できない場合は、エラーを返さないといけません。

libfuseやfuse-rsの説明では、「もし要求されたサイズより短いサイズのデータを返した場合、0埋めされる」と書いてありますが、
手元の環境では0埋めされず、短いサイズが `read(2)` の結果として返ってきました。
[カーネルのこのコミット](https://github.com/torvalds/linux/commit/5c5c5e51b26413d50a9efae2ca7d6c5c6cd453ac#diff-a00aec43f56686c876d5fec8bb227e10) で仕様が変わっているように見えます。

例外として、 `direct_io` をマウントオプションとして指定していた場合、または `direct_io` フラグを `open` の戻り値として指定した場合、
カーネルは `read(2)` システムコールの戻り値としてファイルシステムの戻り値を直接使うので、ファイルシステムは実際に読み込んだ長さを返します。
諸事情(ストリーミングしてる等の理由)でファイルサイズと実際のデータの長さが異なる場合に、このオプションが利用できます。

引数の `fh` は `open` 時に戻り値としてファイルシステムが指定した値です。同じファイルに対して複数の `open` が来たときに、
どの `open` に対しての `read` かを識別したり、ファイルオープン毎に状態を持つことができます。
今回は `open` を実装していないので常に0が来ます。

```rust
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, _size: u32, reply: ReplyData) {
    let mut data: Vec<u8> = Vec::with_capacity(_size as usize);
    let block_size: u32 = self.db.get_db_block_size();
    let mut size: u32 = _size;
    let mut offset: u32 = _offset as u32;
    // sizeが0になるまで1ブロックずつ読み込む
    while size > 0 {
        let mut b_data = match self.db.get_data(ino as u32, offset / block_size + 1, block_size) {
            Ok(n) => n,
            Err(_err) => {reply.error(ENOENT); return; }
        };
        // ブロックの途中から読み込む、またはブロックの途中まで読む場合の対応
        let b_offset: u32 = offset % block_size;
        let b_end: u32 = if (size + b_offset) / block_size >= 1 {block_size} else {size + b_offset};
        if b_data.len() < b_end as usize {
            b_data.resize(b_end as usize, 0);
        }
        data.append(&mut b_data[b_offset as usize..b_end as usize].to_vec());
        offset += b_end - b_offset;
        size -= b_end - b_offset;
    }
    reply.data(&data);
}
```

## readdir
```rust
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory);
```

指定されたinodeのディレクトリのディレクトリエントリを返します。
ディレクトリ内の全てのファイルまたはディレクトリの、「名前」、「ファイル種別(ファイル/ディレクトリ/シンボリックリンク/etc.)」、「inode番号」を返します。
`ls` コマンドの結果を返すイメージです。
一定サイズのバッファが渡されるので、一杯になるまでディレクトリエントリを入れて返します。

引数の `fh` は `opendir` でファイルシステムが渡した値です。今回は `opendir` を実装していないので0です。

cでは `fuse_add_direntry()` という関数を使用してバッファを埋めますが、rustでは引数で渡された `reply: ReplyDirectory` を使用します。
具体的には以下のように使います。

```rust
let target_inode = 11; // inode番号
let filename = "test.txt"; // ファイル名
let fileType = FileType.RegularFile; //ファイル種別
result = reply.add(target_inode, offset, fileType, filename);
```

`reply.add()` でバッファにデータを追加していき、最終的に `reply.ok()` を実行すると、データが返せます。

バッファが一杯の時、 `ReplyDirectory.add()` は `true` を返します。

`reply.add()` の引数の `offset` はファイルシステムが任意に決めたオフセットです。
大抵はディレクトリエントリ一覧内のインデックスや次のエントリへのポインタ(cの場合)が使われます。
同じディレクトリエントリ内で `offset` は一意でなければなりません。また、offsetは決まった順番を持たなければなりません。
カーネルが`readdir`の 引数として `offset` に0でない値を指定してきた場合、
該当の `offset` を持つディレクトリエントリの、次のディレクトリエントリを返さなければならないからです。
`readdir` の引数に `0` が来た場合「最初のディレクトリエントリ」を返さないといけないので、ファイルシステムは `offset` に0を入れてはならないです。

厳密に実装する場合、 `opendir` 時の状態を返さないといけないので、 `opendir` の実装と状態の保持が必要になります。

`. (カレントディレクトリ)` と `.. (親ディレクトリ)` の情報は返さなくともよいですが、返さなかった場合の処理は呼び出し側のプログラムに依存します。基本的には返すようにするといいです。

```rust
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
    let db_entries: Vec<DEntry> = match self.db.get_dentry(ino as u32) {
        Ok(n) => n,
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };
    for (i, entry) in db_entries.into_iter().enumerate().skip(offset as usize) {
        let full = reply.add(entry.child_ino as u64, (i + 1) as i64, entry.file_type, &entry.filename);
        if full {
            break;
        }
    }
    reply.ok();
}
```

## マウント
main関数で `fuse::mount()` を実行すると、マウントできます。

fuse-rsは [env_logger](https://github.com/sebasmagri/env_logger/) に対応しているので、最初に有効にしておきます。
`RUST_LOG=debug [コマンド]` のように、環境変数でレベルを設定できます。
`DEBUG` レベルにすると各関数の呼び出しを記録してくれます。

引数は雑に取得していますが、マウントオプションの処理などがあるので、後々 [clap](https://github.com/clap-rs/clap) などを使って解析することにします。

```rust
fn main() {
    // ログを有効にする
    env_logger::init();
    // 引数からマウントポイントを取得
    let mountpoint = env::args_os().nth(1).unwrap();
    // 引数からDBファイルのパスを取得
    let db_path = env::args_os().nth(2).unwrap();
    // マウントオプションの設定
    let options = ["-o", "ro", "-o", "fsname=sqlitefs"]
        .iter()
        .map(|o| o.as_ref())
        .collect::<Vec<&OsStr>>();
    // ファイルシステムの初期化
    let fs: SqliteFs = match SqliteFs::new(db_path.to_str().unwrap()) {
        Ok(n) => n,
        Err(err) => {println!("{:?}", err); return;}
    };
    // マウント
    fuse::mount(fs, &mountpoint, &options).unwrap();
}
```

## 初期データ登録
自動でテーブルを作成する機能をまだ実装していません。初期化用の [init.sql](https://github.com/narumatt/sqlitefs/blob/hello/init.sql) と、 hello.txt追加用の [hello.sql](https://github.com/narumatt/sqlitefs/blob/hello/hello.sql) を作成して、実行してデータベースを作成します。

`init.sql` ではテーブルの作成とルートディレクトリのメタデータ、ディレクトリエントリの登録、
`hello.sql` では `hello.txt` のメタデータ作成、ルートディレクトリへの追加、データの追加を行っています。

```text
$ sqlite3 ~/filesystem.sqlite < init.sql
$ sqlite3 ~/filesystem.sqlite < hello.sql
```

## ビルド及び実行
`[プログラム名] [マウント先] [データベースファイル名]` で実行できます。

```text:コマンド
# バックグラウンドで実行
$ ./sqlite-fs ~/mount ~/filesystem.sqlite &
# lsしてhello.txtがあるのを確認
$ ls ~/mount
hello.txt
# hello.txtの内容が読み込めることを確認
$ cat ~/mount/hello.txt
Hello World!
```

また、 `$ RUST_LOG=debug cargo run ~/mount` でビルドと実行( `~/mount` にマウントして、デバッグログを出力)ができます。
試しに `cat ~/mount/hello.txt` を実行すると、以下のようなログが出力されます。 `env_logger` のおかげで各関数に対する呼び出しが記録されています。

```text:ログ
[2019-10-25T10:43:27Z DEBUG fuse::request] INIT(2)   kernel: ABI 7.31, flags 0x3fffffb, max readahead 131072
[2019-10-25T10:43:27Z DEBUG fuse::request] INIT(2) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
[2019-10-25T10:43:42Z DEBUG fuse::request] LOOKUP(4) parent 0x0000000000000001, name "hello.txt"
[2019-10-25T10:43:42Z DEBUG fuse::request] OPEN(6) ino 0x0000000000000002, flags 0x8000
[2019-10-25T10:43:42Z DEBUG fuse::request] READ(8) ino 0x0000000000000002, fh 0, offset 0, size 4096
[2019-10-25T10:43:42Z DEBUG fuse::request] FLUSH(10) ino 0x0000000000000002, fh 0, lock owner 12734418937618606797
[2019-10-25T10:43:42Z DEBUG fuse::request] RELEASE(12) ino 0x0000000000000002, fh 0, flags 0x8000, release flags 0x0, lock owner 0
```

lookup -> open -> read -> close の順で関数が呼び出されている事が分かります。
`close` に対応する関数である `flush` と `release` は実装していませんが、動作しています。

## ファイルシステムのアンマウント
ファイルシステムは `fusermount -u [マウント先]` でアンマウントできます。アンマウントするとプログラムは終了します。
`Ctrl + c` 等でプログラムを終了した場合でもマウントしたままになっているので、かならず `fusermount` を実行してください。

## まとめ
4つの関数を実装するだけで、Readonlyのファイルシステムが作成できました。
次回はファイルにデータの書き込みができるようにします。

ここまでのコードは [github](https://github.com/narumatt/sqlitefs/tree/hello) に置いてあります。

**次の記事**
[Rustで学ぶFUSE (2) ファイルの書き込み](https://qiita.com/narumatt/items/5b15dbabfeef32bfaf5c)