mod fuse_daemon;
use btconfig::{CredStoreConfig, FigmentExt, NodeCredConsumer};
use figment::{providers::Serialized, Figment};
use fuse_daemon::FuseDaemon;
mod fuse_fs;
use btfproto::{client::FsClient, local_fs::LocalFs, server::FsProvider};
use btlib::{
    bterr,
    crypto::{Creds, CredsPriv},
    Result,
};
use bttp::{BlockAddr, Transmitter};
use serde::{Deserialize, Serialize};
use std::{
    fs::{self},
    io,
    num::NonZeroUsize,
    path::{Path, PathBuf},
    sync::Arc,
};
use tokio::sync::oneshot;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum FsKind {
    Local { path: PathBuf },
    Remote { addr: BlockAddr },
}
impl Default for FsKind {
    fn default() -> Self {
        Self::Local {
            path: btconfig::default_block_dir(),
        }
    }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BtfuseConfig {
    #[serde(rename = "credstore")]
    pub cred_store: CredStoreConfig,
    #[serde(rename = "fskind")]
    pub fs_kind: FsKind,
    #[serde(rename = "mntdir")]
    pub mnt_dir: PathBuf,
    #[serde(rename = "mntoptions")]
    pub mnt_options: String,
    pub threads: Option<NonZeroUsize>,
}
impl BtfuseConfig {
    pub fn new() -> Result<Self> {
        Figment::new()
            .merge(Serialized::defaults(BtfuseConfig::default()))
            .btconfig()?
            .extract()
            .map_err(|err| err.into())
    }
}
impl Default for BtfuseConfig {
    fn default() -> Self {
        Self {
            cred_store: CredStoreConfig::default(),
            fs_kind: FsKind::default(),
            mnt_dir: "./btfuse_mnt".into(),
            mnt_options: "default_permissions".into(),
            threads: None,
        }
    }
}
trait PathExt {
    fn try_create_dir(&self) -> io::Result<()>;
}
impl<T: AsRef<Path>> PathExt for T {
    fn try_create_dir(&self) -> io::Result<()> {
        match fs::create_dir(self) {
            Ok(_) => Ok(()),
            Err(err) => match err.kind() {
                io::ErrorKind::AlreadyExists => Ok(()),
                _ => Err(err),
            },
        }
    }
}
async fn local_provider(btdir: PathBuf, node_creds: Arc<dyn Creds>) -> Result<impl FsProvider> {
    btdir.try_create_dir()?;
    let empty = fs::read_dir(&btdir)?.next().is_none();
    if empty {
        LocalFs::new_empty(btdir, 0, node_creds, btfproto::local_fs::ModeAuthorizer {}).await
    } else {
        LocalFs::new_existing(btdir, node_creds, btfproto::local_fs::ModeAuthorizer {})
    }
}
async fn remote_provider<C: 'static + Creds + Send + Sync>(
    remote_addr: BlockAddr,
    node_creds: C,
) -> Result<impl FsProvider> {
    let tx = Transmitter::new(Arc::new(remote_addr), Arc::new(node_creds)).await?;
    let client = FsClient::new(tx);
    Ok(client)
}
async fn run_daemon(config: BtfuseConfig, mounted_signal: Option<oneshot::Sender<()>>) {
    let node_creds = config
        .cred_store
        .consume(NodeCredConsumer)
        .unwrap()
        .unwrap();
    let fallback_path = {
        let writecap = node_creds
            .writecap()
            .ok_or(btlib::BlockError::MissingWritecap)
            .unwrap();
        Arc::new(writecap.bind_path())
    };
    let mut daemon = match config.fs_kind {
        FsKind::Local { path: btdir } => {
            log::info!("starting daemon with local provider using {:?}", btdir);
            let provider = local_provider(btdir.clone(), node_creds)
                .await
                .map_err(|err| {
                    bterr!(
                        "failed to create local provider using path '{}': {err}",
                        btdir.display()
                    )
                })
                .unwrap();
            FuseDaemon::new(
                config.mnt_dir,
                &config.mnt_options,
                config.threads,
                fallback_path,
                mounted_signal,
                provider,
            )
        }
        FsKind::Remote { addr: remote_addr } => {
            log::info!(
                "starting daemon with remote provider using {:?}",
                remote_addr.socket_addr()
            );
            let provider = remote_provider(remote_addr, node_creds)
                .await
                .expect("failed to create remote provider");
            FuseDaemon::new(
                config.mnt_dir,
                &config.mnt_options,
                config.threads,
                fallback_path,
                mounted_signal,
                provider,
            )
        }
    }
    .expect("failed to create FUSE daemon");
    daemon.finished().await;
}
#[tokio::main]
async fn main() {
    env_logger::init();
    let config = BtfuseConfig::new().unwrap();
    run_daemon(config, None).await;
}
#[cfg(test)]
mod test {
    use super::*;
    use btconfig::CredStoreConfig;
    use btfproto::{local_fs::ModeAuthorizer, server::new_fs_server};
    use btlib::{
        crypto::{tpm::TpmCredStore, CredStore, CredStoreMut, Creds},
        log::BuilderExt,
        Epoch, Principaled,
    };
    use bttp::Receiver;
    use ctor::ctor;
    use std::{
        ffi::{OsStr, OsString},
        fs::Permissions,
        io::{BufRead, SeekFrom},
        net::{IpAddr, Ipv6Addr},
        num::NonZeroUsize,
        os::unix::fs::PermissionsExt,
        time::Duration,
    };
    use swtpm_harness::SwtpmHarness;
    use tempdir::TempDir;
    use tokio::{
        fs::{
            create_dir, hard_link, metadata, read, read_dir, remove_dir, remove_file, rename,
            set_permissions, write, OpenOptions, ReadDir,
        },
        io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
        task::JoinHandle,
    };
    const TIMEOUT: Option<Duration> = Some(Duration::from_millis(1250));
    const LOG_LEVEL: &str = "warn";
    #[ctor]
    fn ctor() {
        std::env::set_var("RUST_LOG", LOG_LEVEL);
        env_logger::Builder::from_default_env().btformat().init();
    }
    fn mounted(mnt_path: &str) -> bool {
        let file = std::fs::OpenOptions::new()
            .read(true)
            .write(false)
            .create(false)
            .open("/etc/mtab")
            .unwrap();
        let mut reader = std::io::BufReader::new(file);
        let mut line = String::with_capacity(64);
        loop {
            line.clear();
            let read = reader.read_line(&mut line).unwrap();
            if 0 == read {
                break;
            }
            let path = line.split(' ').skip(1).next().unwrap();
            if path == mnt_path {
                return true;
            }
        }
        false
    }
    fn unmount<P: AsRef<Path>>(mnt_path: P) {
        let mnt_path = mnt_path.as_ref();
        if !mounted(mnt_path.to_str().unwrap()) {
            return;
        }
        const PROG: &str = "fusermount";
        let mnt_path = mnt_path
            .as_os_str()
            .to_str()
            .expect("failed to convert mnt_path to `str`");
        let code = std::process::Command::new(PROG)
            .args(["-z", "-u", mnt_path])
            .status()
            .expect("waiting for exit status failed")
            .code()
            .expect("code returned None");
        if code != 0 {
            panic!("{PROG} exited with a non-zero status: {code}");
        }
    }
    async fn file_names(mut read_dir: ReadDir) -> Vec<OsString> {
        let mut output = Vec::new();
        while let Some(entry) = read_dir.next_entry().await.unwrap() {
            output.push(entry.file_name());
        }
        output
    }
    const ROOT_PASSWD: &str = "password";
    struct TestCase {
        config: BtfuseConfig,
        handle: Option<JoinHandle<()>>,
        node_principal: OsString,
        stop_flag: Option<()>,
        _receiver: Option<Receiver>,
        _cred_store: TpmCredStore,
        _swtpm: SwtpmHarness,
        _temp_dir: TempDir,
    }
    async fn new_local() -> TestCase {
        new(false).await
    }
    async fn new_remote() -> TestCase {
        new(true).await
    }
    async fn new(remote: bool) -> TestCase {
        let tmp = TempDir::new("btfuse").unwrap();
        let (mounted_tx, mounted_rx) = oneshot::channel();
        let (swtpm, cred_store) = swtpm();
        let block_dir = tmp.path().join("bt");
        let (fs_kind, receiver) = if remote {
            let node_creds = Arc::new(cred_store.node_creds().unwrap());
            let bind_path = node_creds.bind_path().unwrap();
            block_dir.try_create_dir().unwrap();
            let local_fs = LocalFs::new_empty(block_dir, 0, node_creds.clone(), ModeAuthorizer)
                .await
                .unwrap();
            let ip_addr = IpAddr::V6(Ipv6Addr::LOCALHOST);
            let receiver = new_fs_server(ip_addr, node_creds.clone(), Arc::new(local_fs)).unwrap();
            let fs_kind = FsKind::Remote {
                addr: BlockAddr::new(ip_addr, Arc::new(bind_path)),
            };
            (fs_kind, Some(receiver))
        } else {
            (FsKind::Local { path: block_dir }, None)
        };
        let config = BtfuseConfig {
            threads: Some(NonZeroUsize::new(1).unwrap()),
            mnt_dir: tmp.path().join("mnt"),
            cred_store: CredStoreConfig::Tpm {
                path: swtpm.state_path().to_owned().into(),
                tabrmd: swtpm.tabrmd_config().to_owned(),
            },
            fs_kind,
            mnt_options: "default_permissions".to_string(),
        };
        let config_clone = config.clone();
        let handle = tokio::spawn(async move {
            run_daemon(config_clone, Some(mounted_tx)).await;
        });
        if let Some(timeout) = TIMEOUT {
            tokio::time::timeout(timeout, mounted_rx)
                .await
                .unwrap()
                .unwrap();
        } else {
            mounted_rx.await.unwrap();
        }
        let node_principal =
            OsString::from(cred_store.node_creds().unwrap().principal().to_string());
        TestCase {
            config,
            handle: Some(handle),
            node_principal,
            stop_flag: Some(()),
            _receiver: receiver,
            _temp_dir: tmp,
            _swtpm: swtpm,
            _cred_store: cred_store,
        }
    }
    fn swtpm() -> (SwtpmHarness, TpmCredStore) {
        let swtpm = SwtpmHarness::new().unwrap();
        let state_path: PathBuf = swtpm.state_path().to_owned();
        let cred_store = {
            let context = swtpm.context().unwrap();
            TpmCredStore::from_context(context, state_path.clone()).unwrap()
        };
        let root_creds = cred_store.gen_root_creds(ROOT_PASSWD).unwrap();
        let mut node_creds = cred_store.node_creds().unwrap();
        let expires = Epoch::now() + Duration::from_secs(3600);
        let writecap = root_creds
            .issue_writecap(node_creds.principal(), &mut std::iter::empty(), expires)
            .unwrap();
        cred_store
            .assign_node_writecap(&mut node_creds, writecap)
            .unwrap();
        (swtpm, cred_store)
    }
    impl TestCase {
        fn mnt_dir(&self) -> &Path {
            &self.config.mnt_dir
        }
        fn signal_stop(&mut self) {
            if let Some(_) = self.stop_flag.take() {
                unmount(&self.config.mnt_dir)
            }
        }
        async fn stopped(&mut self) {
            if let Some(handle) = self.handle.take() {
                handle.await.expect("join failed");
            }
        }
        async fn stop(&mut self) {
            self.signal_stop();
            self.stopped().await;
        }
        fn initial_contents(&self) -> Vec<&OsStr> {
            vec![&self.node_principal]
        }
    }
    impl Drop for TestCase {
        fn drop(&mut self) {
            self.signal_stop()
        }
    }
    #[allow(dead_code)]
    async fn manual_test() {
        let mut case = new_local().await;
        case.stopped().await
    }
    async fn write_read(mut case: TestCase) -> Result<()> {
        const EXPECTED: &[u8] =
            b"The paths to failure are uncountable, yet to success there are few.";
        let file_path = case.mnt_dir().join("file");
        write(&file_path, EXPECTED).await?;
        let actual = read(&file_path).await?;
        assert_eq!(EXPECTED, actual);
        case.stop().await;
        Ok(())
    }
    #[tokio::test]
    async fn write_read_local() -> Result<()> {
        write_read(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn write_read_remote() -> Result<()> {
        write_read(new_remote().await).await
    }
    async fn create_file_then_readdir(mut case: TestCase) {
        const DATA: &[u8] = b"Au revoir Shoshanna!";
        let file_name = OsStr::new("landa_dialog.txt");
        let mut expected = case.initial_contents();
        expected.push(file_name);
        let mnt_path = case.mnt_dir();
        let file_path = mnt_path.join(file_name);
        write(&file_path, DATA).await.expect("write failed");
        let first = file_names(read_dir(&mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(expected, first);
        let second = file_names(read_dir(&mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(expected, second);
        case.stop().await;
    }
    #[tokio::test]
    async fn create_file_then_readdir_local() {
        create_file_then_readdir(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn create_file_then_readdir_remote() {
        create_file_then_readdir(new_remote().await).await
    }
    async fn create_then_delete_file(mut case: TestCase) {
        const DATA: &[u8] = b"The universe is hostile, so impersonal. Devour to survive";
        let file_name = OsStr::new("tool_lyrics.txt");
        let mnt_path = case.mnt_dir();
        let file_path = mnt_path.join(file_name);
        write(&file_path, DATA).await.expect("write failed");
        remove_file(&file_path).await.expect("remove_file failed");
        let expected = case.initial_contents();
        let actual = file_names(read_dir(&mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(expected, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn create_then_delete_file_local() {
        create_then_delete_file(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn create_then_delete_file_remote() {
        create_then_delete_file(new_remote().await).await
    }
    async fn hard_link_then_remove(mut case: TestCase) {
        const EXPECTED: &[u8] = b"And the lives we've reclaimed";
        let name1 = OsStr::new("refugee_lyrics.txt");
        let name2 = OsStr::new("rise_against_lyrics.txt");
        let mnt_path = case.mnt_dir();
        let path1 = mnt_path.join(name1);
        let path2 = mnt_path.join(name2);
        write(&path1, EXPECTED).await.expect("write failed");
        hard_link(&path1, &path2).await.expect("hard_link failed");
        remove_file(&path1).await.expect("remove_file failed");
        let actual = read(&path2).await.expect("read failed");
        assert_eq!(EXPECTED, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn hard_link_then_remove_local() {
        hard_link_then_remove(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn hard_link_then_remove_remote() {
        hard_link_then_remove(new_remote().await).await
    }
    async fn hard_link_then_remove_both(mut case: TestCase) {
        const EXPECTED: &[u8] = b"And the lives we've reclaimed";
        let name1 = OsStr::new("refugee_lyrics.txt");
        let name2 = OsStr::new("rise_against_lyrics.txt");
        let mnt_path = case.mnt_dir();
        let path1 = mnt_path.join(name1);
        let path2 = mnt_path.join(name2);
        write(&path1, EXPECTED).await.expect("write failed");
        hard_link(&path1, &path2).await.expect("hard_link failed");
        remove_file(&path1)
            .await
            .expect("remove_file on path1 failed");
        remove_file(&path2)
            .await
            .expect("remove_file on path2 failed");
        let expected = case.initial_contents();
        let actual = file_names(read_dir(&mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(expected, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn hard_link_then_remove_both_local() {
        hard_link_then_remove_both(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn hard_link_then_remove_both_remote() {
        hard_link_then_remove_both(new_remote().await).await
    }
    async fn set_mode_bits(mut case: TestCase) {
        const EXPECTED: u32 = libc::S_IFREG | 0o777;
        let file_path = case.mnt_dir().join("bagobits");
        write(&file_path, []).await.expect("write failed");
        let original = metadata(&file_path)
            .await
            .expect("metadata failed")
            .permissions()
            .mode();
        assert_ne!(EXPECTED, original);
        set_permissions(&file_path, Permissions::from_mode(EXPECTED))
            .await
            .expect("set_permissions failed");
        let actual = metadata(&file_path)
            .await
            .expect("metadata failed")
            .permissions()
            .mode();
        assert_eq!(EXPECTED, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn set_mode_bits_local() {
        set_mode_bits(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn set_mode_bits_remote() {
        set_mode_bits(new_remote().await).await
    }
    async fn create_directory(mut case: TestCase) {
        const EXPECTED: &str = "etc";
        let mnt_path = case.mnt_dir();
        let dir_path = mnt_path.join(EXPECTED);
        let mut expected = case.initial_contents();
        expected.push(OsStr::new(EXPECTED));
        create_dir(&dir_path).await.expect("create_dir failed");
        let actual = file_names(read_dir(mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(expected, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn create_directory_local() {
        create_directory(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn create_directory_remote() {
        create_directory(new_remote().await).await
    }
    async fn create_file_under_new_directory(mut case: TestCase) {
        const DIR_NAME: &str = "etc";
        const FILE_NAME: &str = "file";
        let mnt_path = case.mnt_dir();
        let dir_path = mnt_path.join(DIR_NAME);
        let file_path = dir_path.join(FILE_NAME);
        create_dir(&dir_path).await.expect("create_dir failed");
        write(&file_path, []).await.expect("write failed");
        let actual = file_names(read_dir(dir_path).await.expect("read_dir failed")).await;
        assert_eq!([FILE_NAME].as_slice(), actual.as_slice());
        case.stop().await;
    }
    #[tokio::test]
    async fn create_file_under_new_directory_local() {
        create_file_under_new_directory(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn create_file_under_new_directory_remote() {
        create_file_under_new_directory(new_remote().await).await
    }
    async fn create_then_remove_directory(mut case: TestCase) {
        const DIR_NAME: &str = "etc";
        let mnt_path = case.mnt_dir();
        let dir_path = mnt_path.join(DIR_NAME);
        create_dir(&dir_path).await.expect("create_dir failed");
        remove_dir(&dir_path).await.expect("remove_dir failed");
        let actual = file_names(read_dir(&mnt_path).await.expect("read_dir failed")).await;
        assert_eq!(case.initial_contents(), actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn create_then_remote_directory_local() {
        create_then_remove_directory(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn create_then_remote_directory_remote() {
        create_then_remove_directory(new_remote().await).await
    }
    async fn read_only_dir_cant_create_subdir(mut case: TestCase) {
        const DIR_NAME: &str = "etc";
        let dir_path = case.mnt_dir().join(DIR_NAME);
        create_dir(&dir_path).await.expect("create_dir failed");
        set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
            .await
            .expect("set_permissions failed");
        let result = create_dir(dir_path.join("sub")).await;
        let err = result.err().expect("create_dir returned `Ok`");
        let os_err = err.raw_os_error().expect("raw_os_error was empty");
        assert_eq!(os_err, libc::EACCES);
        case.stop().await;
    }
    #[tokio::test]
    async fn read_only_dir_cant_create_subdir_local() {
        read_only_dir_cant_create_subdir(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn read_only_dir_cant_create_subdir_remote() {
        read_only_dir_cant_create_subdir(new_remote().await).await
    }
    async fn read_only_dir_cant_remove_subdir(mut case: TestCase) {
        const DIR_NAME: &str = "etc";
        let dir_path = case.mnt_dir().join(DIR_NAME);
        let sub_path = dir_path.join("sub");
        create_dir(&dir_path).await.expect("create_dir failed");
        create_dir(&sub_path).await.expect("create_dir failed");
        set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
            .await
            .expect("set_permissions failed");
        let result = remove_dir(&sub_path).await;
        let err = result.err().expect("remove_dir returned `Ok`");
        let os_err = err.raw_os_error().expect("raw_os_error was empty");
        assert_eq!(os_err, libc::EACCES);
        case.stop().await;
    }
    #[tokio::test]
    async fn read_only_dir_cant_remove_subdir_local() {
        read_only_dir_cant_remove_subdir(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn read_only_dir_cant_remove_subdir_remote() {
        read_only_dir_cant_remove_subdir(new_remote().await).await
    }
    async fn rename_file(mut case: TestCase) {
        const FILE_NAME: &str = "parabola.txt";
        const EXPECTED: &[u8] = b"We are eternal all this pain is an illusion";
        let src_path = case.mnt_dir().join(FILE_NAME);
        let dst_path = case.mnt_dir().join("parabola_lyrics.txt");
        write(&src_path, EXPECTED).await.unwrap();
        rename(&src_path, &dst_path).await.unwrap();
        let actual = read(&dst_path).await.unwrap();
        assert_eq!(EXPECTED, actual);
        case.stop().await;
    }
    #[tokio::test]
    async fn rename_file_local() {
        rename_file(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn rename_file_remote() {
        rename_file(new_remote().await).await
    }
    async fn write_read_with_file_struct(mut case: TestCase) {
        const FILE_NAME: &str = "big.dat";
        const LEN: usize = btlib::SECTOR_SZ_DEFAULT + 1;
        fn fill(buf: &mut Vec<u8>, value: u8) {
            buf.clear();
            buf.extend(std::iter::repeat(value).take(buf.capacity()));
        }
        let file_path = case.mnt_dir().join(FILE_NAME);
        let mut buf = vec![1u8; LEN];
        let mut file = OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .open(&file_path)
            .await
            .unwrap();
        file.write_all(&buf).await.unwrap();
        fill(&mut buf, 2);
        file.write_all(&buf).await.unwrap();
        file.rewind().await.unwrap();
        let mut actual = vec![0u8; LEN];
        file.read_exact(&mut actual).await.unwrap();
        fill(&mut buf, 1);
        assert_eq!(buf, actual);
        drop(file);
        case.stop().await;
    }
    #[tokio::test]
    async fn write_read_with_file_struct_local() {
        write_read_with_file_struct(new_local().await).await
    }
    #[tokio::test(flavor = "multi_thread")]
    async fn write_read_with_file_struct_remote() {
        write_read_with_file_struct(new_remote().await).await
    }
    async fn read_more_than_whats_buffered(mut case: TestCase) {
        const FILE_NAME: &str = "big.dat";
        const SECT_SZ: usize = btlib::SECTOR_SZ_DEFAULT;
        const DIVISOR: usize = 8;
        const READ_SZ: usize = SECT_SZ / DIVISOR;
        let file_path = case.mnt_dir().join(FILE_NAME);
        let mut file = OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .open(&file_path)
            .await
            .unwrap();
        let mut buf = vec![1u8; 2 * SECT_SZ];
        file.write_all(&buf).await.unwrap();
        file.flush().await.unwrap();
        let mut file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(&file_path)
            .await
            .unwrap();
        file.seek(SeekFrom::Start(SECT_SZ as u64)).await.unwrap();
        let mut actual = vec![0u8; READ_SZ];
        file.read_exact(&mut actual).await.unwrap();
        buf.truncate(READ_SZ);
        assert!(buf == actual);
        case.stop().await;
    }
    #[allow(dead_code)]
    async fn read_more_than_whats_buffered_local() {
        read_more_than_whats_buffered(new_local().await).await
    }
    #[allow(dead_code)]
    async fn read_more_than_whats_buffered_remote() {
        read_more_than_whats_buffered(new_remote().await).await
    }
}
#[cfg(test)]
mod config_tests {
    use super::{BtfuseConfig, FsKind};
    use std::{net::IpAddr, num::NonZeroUsize, path::PathBuf, sync::Arc};
    use btconfig::CredStoreConfig;
    use btlib::BlockPath;
    use bttp::BlockAddr;
    use figment::Jail;
    #[test]
    fn fs_kind_local() {
        Jail::expect_with(|jail| {
            const EXPECTED_PATH: &str = "/tmp/blocks";
            let expected = FsKind::Local {
                path: EXPECTED_PATH.into(),
            };
            jail.set_env("BT_FSKIND_TYPE", "Local");
            jail.set_env("BT_FSKIND_PATH", EXPECTED_PATH);
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, config.fs_kind);
            Ok(())
        })
    }
    #[test]
    fn fs_kind_remote() {
        Jail::expect_with(|jail| {
            let expected_ip = IpAddr::from([127, 0, 0, 42]);
            let expected_path = Arc::new(BlockPath::try_from(
                "/0!zX_LMUVQO2Y7mgDomQB8ZdNsXKlykpHs-zPX9C3ztII/0!vVB5rOb3NFjzaZl_wlH3jqhBaYV7uuxrk3_s42xLnzg"
            ).unwrap());
            let expected_addr = BlockAddr::new(expected_ip, expected_path.clone());
            let expected = FsKind::Remote {
                addr: expected_addr,
            };
            jail.set_env("BT_FSKIND_TYPE", "Remote");
            jail.set_env("BT_FSKIND_ADDR_IPADDR", expected_ip);
            jail.set_env("BT_FSKIND_ADDR_PATH", expected_path.as_ref());
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, config.fs_kind);
            Ok(())
        })
    }
    #[test]
    fn mnt_dir() {
        Jail::expect_with(|jail| {
            let expected = PathBuf::from("/tmp/btfuse_mnt");
            jail.set_env("BT_MNTDIR", expected.display());
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, config.mnt_dir);
            Ok(())
        })
    }
    #[test]
    fn mnt_options() {
        Jail::expect_with(|jail| {
            let expected = "default_permissions";
            jail.set_env("BT_MNTOPTIONS", expected);
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, &config.mnt_options);
            Ok(())
        })
    }
    #[test]
    fn threads_is_set() {
        Jail::expect_with(|jail| {
            let expected = Some(NonZeroUsize::new(8).unwrap());
            jail.set_env("BT_THREADS", expected.unwrap().get());
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, config.threads);
            Ok(())
        })
    }
    #[test]
    fn threads_is_not_set() {
        Jail::expect_with(|_jail| {
            let expected = None;
            let config = BtfuseConfig::new().unwrap();
            assert_eq!(expected, config.threads);
            Ok(())
        })
    }
    #[test]
    fn cred_store_path() {
        Jail::expect_with(|jail| {
            let expected = PathBuf::from("/tmp/secrets/file_credstore");
            jail.set_env("BT_CREDSTORE_TYPE", "File");
            jail.set_env("BT_CREDSTORE_PATH", expected.display());
            let config = BtfuseConfig::new().unwrap();
            let success = if let CredStoreConfig::File { path: actual } = config.cred_store {
                expected == actual
            } else {
                false
            };
            assert!(success);
            Ok(())
        })
    }
}