Thursday | 28 MAR 2024
[ previous ]
[ next ]

Rust, Fuse, Sqlite - 03 real readdir

Title:
Date: 2023-02-09
Tags:  

A working example where tables are displayed and rows and shown as files. Very hacky and doesn't feel right. Rust is rust smh.

use rusqlite::{Connection};
use std::collections::HashMap;

use std::time::{Duration, UNIX_EPOCH};
use std::ffi::OsStr;
use fuser::{
    FileAttr, FileType, Filesystem, ReplyAttr, ReplyDirectory, ReplyEntry, Request, MountOption
};

#[derive(Debug)]
struct Table {
    name: String,
}

struct RFS {
    name_map: HashMap<String, FileAttr>, 
    inode_map: HashMap<u64, String>,
}

const TTL: Duration = Duration::from_secs(1);

fn generate_inode_dir(ino: u64) -> FileAttr {
    FileAttr {
        ino: ino,
        size: 4096,
        blocks: 0,
        atime: UNIX_EPOCH,
        mtime: UNIX_EPOCH,
        ctime: UNIX_EPOCH,
        crtime: UNIX_EPOCH,
        kind: FileType::Directory,
        perm: 0o755,
        nlink: 2,
        uid: 1000,
        gid: 1000,
        rdev: 0,
        flags: 0,
        blksize: 512,
        padding: 0,
    }
}

const DEFAULT_DIR_ATTR: FileAttr = FileAttr {
    ino: 2,
    size: 4096,
    blocks: 0,
    atime: UNIX_EPOCH,
    mtime: UNIX_EPOCH,
    ctime: UNIX_EPOCH,
    crtime: UNIX_EPOCH,
    kind: FileType::Directory,
    perm: 0o755,
    nlink: 2,
    uid: 1000,
    gid: 1000,
    rdev: 0,
    flags: 0,
    blksize: 512,
    padding: 0,
};

const DEFAULT_FILE_ATTR: FileAttr = FileAttr {
    ino: 3,
    size: 4096,
    blocks: 1,
    atime: UNIX_EPOCH, 
    mtime: UNIX_EPOCH,
    ctime: UNIX_EPOCH,
    crtime: UNIX_EPOCH,
    kind: FileType::RegularFile,
    perm: 0o644,
    nlink: 1,
    uid: 1000,
    gid: 1000,
    rdev: 0,
    flags: 0,
    blksize: 512,
    padding: 0,
};

impl Filesystem for RFS {
    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
        println!("Calling getattr: {}", ino);

        if let Some(_) = self.inode_map.get(&ino) {
            reply.attr(&TTL, &DEFAULT_DIR_ATTR);

        } else {
            if ino == 1 {
                reply.attr(&TTL, &DEFAULT_DIR_ATTR);
            } else {
                reply.attr(&TTL, &DEFAULT_FILE_ATTR);
            }
        }

    }

    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
        println!("Calling lookup: {:?}, {:?}", name, parent);

        let x: String = name.to_string_lossy().to_string();
        if let Some(t) = self.name_map.get(&x) {
            reply.entry(&TTL, &t, 0);

        } else {
            reply.entry(&TTL, &DEFAULT_FILE_ATTR, 0);
        }
    }

    fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        println!("Calling readdir: {}", ino);

        if ino == 1 {
            let mut entries = vec![
                (1, FileType::Directory, String::from(".")),
                (1, FileType::Directory, String::from("..")),
            ];

            let conn = Connection::open("example.db").unwrap();

            let mut stmt = conn.prepare(
                "select name from sqlite_master 
                where type = 'table' 
                and name not like 'sqlite_%';"
            ).unwrap();

            let tables = stmt.query_map([], |row| {
                Ok(Table {
                    name: row.get(0)?
                })
            }).unwrap();

            let mut inode = 2;
            let mut name_map = HashMap::new();
            let mut inode_map = HashMap::new();

            for table in tables {
                if let Ok(t) = table {
                    let attr = generate_inode_dir(inode);
                    name_map.insert(t.name.clone(), attr);
                    inode_map.insert(inode, t.name.clone());
                    inode = inode + 1;
                }
            }

            self.name_map = name_map;
            self.inode_map = inode_map;

            for (name, _) in &self.name_map {
                entries.push((1, FileType::Directory, name.to_string()));
            }

            for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
                if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) {
                    break;
                }
            }

            reply.ok();

        } else {
            if let Some(table) = self.inode_map.get(&ino) {
                println!("Reading...: {}", table);

                let mut entries = vec![
                    (1, FileType::Directory, String::from(".")),
                    (1, FileType::Directory, String::from("..")),
                ];

                let conn = Connection::open("example.db").unwrap();

                let query = format!("select * from {};", table);
                let mut stmt = conn.prepare(&query).unwrap();

                let tables = stmt.query_map([], |row| {
                    Ok(Table {
                        name: row.get(0)?
                    })
                }).unwrap();

                let inode = 2;
                let mut name_map = HashMap::new();

                for table in tables {
                    if let Ok(t) = table {
                        let attr = generate_inode_dir(inode);
                        name_map.insert(t.name.clone(), attr);
                    }
                }

                for (name, _) in name_map {
                    entries.push((3, FileType::RegularFile, name.to_string()));
                }

                for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
                    if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) {
                        break;
                    }
                }

                reply.ok();

            } else {
                println!("Not a directory!");
            }
        }
    }
}

fn main() {
    let mountpoint = "dir";
    let mut options = vec![];
    options.push(MountOption::AutoUnmount);

    let conn = Connection::open("example.db").unwrap();

    let mut stmt = conn.prepare(
        "select name from sqlite_master 
                where type = 'table' 
                and name not like 'sqlite_%';"
    ).unwrap();

    let tables = stmt.query_map([], |row| {
        Ok(Table {
            name: row.get(0)?
        })
    }).unwrap();

    let mut inode = 2;
    let mut name_map = HashMap::new();
    let mut inode_map = HashMap::new();

    for table in tables {
        if let Ok(t) = table {
            let attr = generate_inode_dir(inode);
            name_map.insert(t.name.clone(), attr);
            inode_map.insert(inode, t.name.clone());
            inode = inode + 1;
        }
    }

    fuser::mount2(RFS { name_map, inode_map }, mountpoint, &options).unwrap();
}