// SPDX-License-Identifier: AGPL-3.0-or-later use btconfig::{CredStoreConfig, FigmentExt, NodeCredConsumer}; use btconsole::{BtConsole, BtConsoleConfig}; use btfproto::{ local_fs::{LocalFs, ModeAuthorizer}, server::{new_fs_server, FsProvider}, }; use btlib::{bterr, crypto::Creds, error::DisplayErr, log::BuilderExt, Result}; use btmsg::Receiver; use figment::{providers::Serialized, Figment}; use serde::{Deserialize, Serialize}; use std::{net::IpAddr, path::PathBuf, sync::Arc}; #[derive(Debug, Serialize, Deserialize, Clone)] struct BtfsdConfig { #[serde(rename = "credstore")] cred_store: CredStoreConfig, /// The IP address to listen for filesystem connection on. #[serde(rename = "ipaddr")] ip_addr: IpAddr, /// The path in the local filesystem where blocks are stored. #[serde(rename = "blockdir")] block_dir: PathBuf, /// Configuration for the btconsole webapp. console: Option, } impl BtfsdConfig { fn new() -> Result { Figment::new() .merge(Serialized::defaults(Self::default())) .btconfig()? .extract() .map_err(|err| err.into()) } } impl Default for BtfsdConfig { fn default() -> Self { Self { cred_store: CredStoreConfig::default(), ip_addr: IpAddr::from([127, 0, 0, 1]), block_dir: btconfig::default_block_dir(), console: Some(BtConsoleConfig::default()), } } } async fn provider(block_dir: PathBuf, creds: Arc) -> Result { if block_dir.exists() { LocalFs::new_existing(block_dir, creds, ModeAuthorizer) } else { std::fs::create_dir_all(&block_dir)?; LocalFs::new_empty(block_dir, 0, creds, ModeAuthorizer).await } } async fn receiver(config: BtfsdConfig) -> Result { let node_creds = config.cred_store.consume(NodeCredConsumer)??; let provider = Arc::new(provider(config.block_dir, node_creds.clone()).await?); new_fs_server(config.ip_addr, Arc::new(node_creds), provider) } #[tokio::main] async fn main() -> Result<()> { env_logger::Builder::from_default_env().btformat().init(); let mut config = BtfsdConfig::new()?; let console_config = config .console .take() .ok_or_else(|| bterr!("No BtConsole configuration was provided"))?; let (console, receiver) = tokio::join!(BtConsole::start(console_config), receiver(config)); let receiver = receiver?; let console = console?; log::debug!("ready to accept connections"); let (console, receiver) = tokio::join!(console.has_shutdown(), receiver.complete()?); console?; receiver.display_err()?; Ok(()) } #[cfg(test)] mod tests { use super::*; use btconfig::CredStoreConfig; use btconsole::BtConsoleConfig; use btfproto::{client::FsClient, msg::*}; use btlib::{ crypto::{ file_cred_store::FileCredStore, tpm::TpmCredStore, ConcreteCreds, CredStore, CredStoreMut, CredsPriv, CredsPub, }, log::BuilderExt, AuthzAttrs, BlockMetaSecrets, Epoch, IssuedProcRec, Principaled, ProcRec, }; use btlib_tests::{CredStoreTestingExt, TpmCredStoreHarness}; use btmsg::BlockAddr; use btserde::from_slice; use std::net::{IpAddr, Ipv4Addr}; use std::{future::ready, net::Ipv6Addr, time::Duration}; use swtpm_harness::SwtpmHarness; use tempdir::TempDir; const LOG_LEVEL: &str = "warn"; #[ctor::ctor] fn ctor() { std::env::set_var("RUST_LOG", LOG_LEVEL); env_logger::Builder::from_default_env().btformat().init(); } struct FileTestCase { client: FsClient, _rx: Receiver, _dir: TempDir, } async fn file_test_case(dir: TempDir, ipaddr: IpAddr) -> FileTestCase { let file_store_path = dir.path().join("cred_store"); let credstore = CredStoreConfig::File { path: file_store_path.clone(), }; { let cred_store = FileCredStore::new(file_store_path).unwrap(); cred_store.provision(ROOT_PASSWD).unwrap(); } let config = BtfsdConfig { cred_store: credstore, ip_addr: ipaddr, block_dir: dir.path().join(BT_DIR), console: Some(BtConsoleConfig::default()), }; let rx = receiver(config).await.unwrap(); let tx = rx.transmitter(rx.addr().clone()).await.unwrap(); let client = FsClient::new(tx); FileTestCase { _dir: dir, _rx: rx, client, } } async fn new_file_test_case() -> FileTestCase { let dir = TempDir::new("btfsd").unwrap(); file_test_case(dir, LOCALHOST).await } struct TestCase { client: FsClient, rx: Receiver, harness: TpmCredStoreHarness, _dir: TempDir, } const ROOT_PASSWD: &str = "existential_threat"; const LOCALHOST: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST); const BT_DIR: &str = "bt"; async fn tpm_test_case(dir: TempDir, harness: TpmCredStoreHarness, ipaddr: IpAddr) -> TestCase { let swtpm = harness.swtpm(); let credstore = CredStoreConfig::Tpm { path: swtpm.state_path().to_owned(), tabrmd: swtpm.tabrmd_config().to_owned(), }; let config = BtfsdConfig { cred_store: credstore, ip_addr: ipaddr, block_dir: dir.path().join(BT_DIR), console: Some(BtConsoleConfig::default()), }; let rx = receiver(config).await.unwrap(); let tx = rx.transmitter(rx.addr().clone()).await.unwrap(); let client = FsClient::new(tx); TestCase { _dir: dir, harness, rx, client, } } async fn new_tpm_test_case() -> TestCase { let dir = TempDir::new("btfsd").unwrap(); let harness = TpmCredStoreHarness::new(ROOT_PASSWD.to_owned()).unwrap(); tpm_test_case(dir, harness, LOCALHOST).await } async fn existing_case(case: TestCase) -> TestCase { case.rx.stop().unwrap(); case.rx.complete().unwrap().await.unwrap(); let TestCase { _dir, harness: _harness, .. } = case; tpm_test_case(_dir, _harness, IpAddr::V4(Ipv4Addr::LOCALHOST)).await } #[allow(dead_code)] async fn manual_test() { let case = new_tpm_test_case().await; case.rx.complete().unwrap().await.unwrap(); } async fn create_write_read(client: FsClient) { const FILENAME: &str = "file.txt"; const EXPECTED: &[u8] = b"potato"; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { let mut buf = Vec::with_capacity(EXPECTED.len()); buf.extend_from_slice(reply.data); ready(buf) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn create_write_read_with_tpm() { let case = new_tpm_test_case().await; create_write_read(case.client).await; } #[tokio::test] async fn create_write_read_with_file() { let case = new_file_test_case().await; create_write_read(case.client).await; } #[tokio::test] async fn read_full_sector() { const FILENAME: &str = "file.txt"; const EXPECTED: &[u8] = b"prawn crisps"; let case = new_tpm_test_case().await; let client = case.client; let CreateReply { inode, handle, entry, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); assert!(entry.attr.sect_sz > EXPECTED.len() as u64); let actual = client .read(inode, handle, 0, entry.attr.sect_sz, |reply| { let mut buf = Vec::with_capacity(EXPECTED.len()); buf.extend_from_slice(reply.data); ready(buf) }) .await .unwrap(); assert!(EXPECTED.eq(&actual)); } #[tokio::test] async fn read_from_different_instance() { const FILENAME: &str = "file.txt"; const EXPECTED: &[u8] = b"potato"; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); client.flush(inode, handle).await.unwrap(); let case = existing_case(case).await; let client = &case.client; let LookupReply { inode, .. } = client .lookup(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { let mut buf = Vec::with_capacity(EXPECTED.len()); buf.extend_from_slice(reply.data); ready(buf) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn create_lookup() { const FILENAME: &str = "file.txt"; let case = new_tpm_test_case().await; let client = case.client; let CreateReply { inode: expected, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadOnly.into(), 0o644, 0, ) .await .unwrap(); let LookupReply { inode: actual, .. } = client .lookup(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn open_existing() { const FILENAME: &str = "file.txt"; let case = new_tpm_test_case().await; let client = case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadOnly.into(), 0o644, 0, ) .await .unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; assert!(result.is_ok()); } #[tokio::test] async fn write_flush_close_read() { const FILENAME: &str = "lyrics.txt"; const EXPECTED: &[u8] = b"Fate, or something better"; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); client.flush(inode, handle).await.unwrap(); client.close(inode, handle).await.unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(reply.data.to_owned()) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn link() { const FIRSTNAME: &str = "Jean-Luc"; const LASTNAME: &str = "Picard"; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FIRSTNAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .link(inode, SpecInodes::RootDir.into(), LASTNAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(filenames.contains(&FIRSTNAME)); assert!(filenames.contains(&LASTNAME)); } #[tokio::test] async fn unlink() { const FIRSTNAME: &str = "Jean-Luc"; const LASTNAME: &str = "Picard"; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FIRSTNAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .link(inode, SpecInodes::RootDir.into(), LASTNAME) .await .unwrap(); client .unlink(SpecInodes::RootDir.into(), FIRSTNAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(filenames.contains(&LASTNAME)); assert!(!filenames.contains(&FIRSTNAME)); } #[tokio::test] async fn delete() { const FILENAME: &str = "MANIFESTO.tex"; let case = new_tpm_test_case().await; let client = &case.client; client .create( SpecInodes::RootDir.into(), FILENAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .unlink(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(!filenames.contains(&FILENAME)); } #[tokio::test] async fn read_meta() { const FILENAME: &str = "kibosh.txt"; const EXPECTED: u32 = 0o600; let case = new_tpm_test_case().await; let client = &case.client; let before_create = Epoch::now(); let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), EXPECTED, 0, ) .await .unwrap(); let before_read = Epoch::now(); let ReadMetaReply { attrs, .. } = client.read_meta(inode, Some(handle)).await.unwrap(); let actual = attrs.mode; assert_eq!(FileType::Reg | EXPECTED, actual); assert!(before_create <= attrs.ctime && attrs.ctime <= before_read); assert!(before_create <= attrs.mtime && attrs.mtime <= before_read); assert!(before_create <= attrs.atime && attrs.atime <= before_read); assert_eq!(attrs.block_id.inode, inode); assert_eq!(attrs.size, 0); assert!(attrs.tags.is_empty()); } #[tokio::test] async fn write_meta() { fn assert_eq(expected: &Attrs, actual: &BlockMetaSecrets) { assert_eq!(expected.mode, actual.mode); assert_eq!(expected.uid, actual.uid); assert_eq!(expected.gid, actual.gid); assert_eq!(expected.atime, actual.atime); assert_eq!(expected.mtime, actual.mtime); assert_eq!(expected.ctime, actual.ctime); assert_eq!(expected.tags.len(), actual.tags.len()); for (key, expected_value) in expected.tags.iter() { let actual_value = actual.tags.get(key).unwrap(); assert_eq!(expected_value, actual_value); } } const FILENAME: &str = "word_salad.docx"; let expected = Attrs { mode: 0o600, uid: 9290, gid: 2190, atime: 23193.into(), mtime: 53432.into(), ctime: 87239.into(), tags: Vec::new(), }; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), expected.mode | 0o011, 0, ) .await .unwrap(); let WriteMetaReply { attrs, .. } = client .write_meta(inode, Some(handle), expected.clone(), AttrsSet::ALL) .await .unwrap(); assert_eq(&expected, &attrs); let ReadMetaReply { attrs, .. } = client.read_meta(inode, Some(handle)).await.unwrap(); assert_eq(&expected, &attrs); } #[tokio::test] async fn allocate_when_empty() { const FILENAME: &str = "output.dat"; const EXPECTED: &[u8] = &[0u8; 8]; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client .allocate(inode, handle, EXPECTED.len() as u64) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(Vec::from(reply.data)) }) .await .unwrap(); assert_eq!(EXPECTED, actual); } #[tokio::test] async fn allocate_with_data_present() { const FILENAME: &str = "output.dat"; const EXPECTED: &[u8] = &[1, 1, 1, 1, 0, 0, 0, 0]; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client.write(inode, handle, 0, &[1, 1, 1, 1]).await.unwrap(); client .allocate(inode, handle, EXPECTED.len() as u64) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(Vec::from(reply.data)) }) .await .unwrap(); assert_eq!(EXPECTED, actual); } #[tokio::test] async fn forget() { const FILENAME: &str = "seed.dat"; let case = new_tpm_test_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client.close(inode, handle).await.unwrap(); let result = client.forget(inode, 1).await; assert!(result.is_ok()); } #[tokio::test] async fn add_readcap() { const FILENAME: &str = "net"; let case = new_tpm_test_case().await; let client = &case.client; let creds = ConcreteCreds::generate().unwrap(); let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite | FlagValue::Directory, 0o755, 0, ) .await .unwrap(); client .add_readcap(inode, handle, creds.concrete_pub()) .await .unwrap(); } #[tokio::test] async fn grant_access_to_root() { let case = new_tpm_test_case().await; let client = &case.client; let mut creds = ConcreteCreds::generate().unwrap(); let root_creds = case.harness.root_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), &mut std::iter::empty(), Epoch::now() + Duration::from_secs(3600), ) .unwrap(); creds.set_writecap(writecap).unwrap(); let expected = IssuedProcRec { addr: IpAddr::V4(Ipv4Addr::LOCALHOST), pub_creds: creds.concrete_pub(), writecap: creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; client .grant_access(SpecInodes::RootDir.into(), expected.clone()) .await .unwrap(); let LookupReply { inode, entry, .. } = client .lookup(SpecInodes::RootDir.into(), &creds.principal().to_string()) .await .unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let record = client .read(inode, handle, 0, entry.attr.size, |reply| { ready(from_slice::(reply.data)) }) .await .unwrap() .unwrap(); let actual = record.validate().unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn grant_access_to_non_root_dir() { const DIRNAME: &str = "var"; let case = new_tpm_test_case().await; let client = &case.client; let mut creds = ConcreteCreds::generate().unwrap(); let root_creds = case.harness.root_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), &mut [DIRNAME].into_iter(), Epoch::now() + Duration::from_secs(3600), ) .unwrap(); creds.set_writecap(writecap).unwrap(); let expected = IssuedProcRec { addr: IpAddr::V4(Ipv4Addr::LOCALHOST), pub_creds: creds.concrete_pub(), writecap: creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), DIRNAME, FlagValue::Directory | FlagValue::ReadWrite, 0o755, 0, ) .await .unwrap(); client.close(inode, handle).await.unwrap(); client.grant_access(inode, expected.clone()).await.unwrap(); let LookupReply { inode: record_inode, entry, .. } = client .lookup(inode, &creds.principal().to_string()) .await .unwrap(); let OpenReply { handle: record_handle, .. } = client .open(record_inode, FlagValue::ReadOnly.into()) .await .unwrap(); let record = client .read(record_inode, record_handle, 0, entry.attr.size, |reply| { ready(from_slice::(reply.data)) }) .await .unwrap() .unwrap(); let actual = record.validate().unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn grant_access_non_root_user() { const ROOT_FILE: &str = "root.txt"; const USER_FILE: &str = "user.txt"; let user_tpm = SwtpmHarness::new().unwrap(); let case = new_tpm_test_case().await; let client = &case.client; let root_creds = case.harness.root_creds().unwrap(); let user_creds = { let cred_store = TpmCredStore::from_context( user_tpm.context().unwrap(), user_tpm.state_path().to_owned(), ) .unwrap(); let mut creds = cred_store.node_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), &mut std::iter::empty(), Epoch::now() + Duration::from_secs(3600), ) .unwrap(); cred_store .assign_node_writecap(&mut creds, writecap) .unwrap(); creds }; let expected = IssuedProcRec { addr: IpAddr::V4(Ipv4Addr::LOCALHOST), pub_creds: user_creds.concrete_pub(), writecap: user_creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; let root_inode = SpecInodes::RootDir.into(); client .grant_access(root_inode, expected.clone()) .await .unwrap(); // Note that non-root users do not have permission to write to ROOT_FILE, but // UID 9001 has permission to write to USER_FILE. client .create(root_inode, ROOT_FILE, FlagValue::ReadWrite.into(), 0o644, 0) .await .unwrap(); let CreateReply { inode, handle, .. } = client .create(root_inode, USER_FILE, FlagValue::ReadWrite.into(), 0o644, 0) .await .unwrap(); let mut attrs = Attrs::default(); attrs.uid = expected.authz_attrs.uid; attrs.gid = expected.authz_attrs.gid; client .write_meta(inode, Some(handle), attrs, AttrsSet::UID | AttrsSet::GID) .await .unwrap(); let node_creds = case.harness.cred_store().node_creds().unwrap(); let bind_path = node_creds.writecap().unwrap().bind_path(); let block_addr = Arc::new(BlockAddr::new(LOCALHOST, Arc::new(bind_path))); let tx = btmsg::Transmitter::new(block_addr, Arc::new(user_creds)) .await .unwrap(); let client = FsClient::new(tx); let root_dir = SpecInodes::RootDir.value(); let LookupReply { inode, .. } = client.lookup(root_dir, USER_FILE).await.unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; assert!(result.is_ok()); let LookupReply { inode, .. } = client.lookup(root_dir, ROOT_FILE).await.unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; let err = result.err().unwrap().to_string(); assert_eq!("write access denied", err); } } #[cfg(test)] mod config_tests { use super::BtfsdConfig; use std::{ net::{IpAddr, SocketAddr}, path::PathBuf, }; use btconfig::CredStoreConfig; use btconsole::BtConsoleConfig; use figment::Jail; #[test] fn cred_store_file_path() { Jail::expect_with(|jail| { let expected = PathBuf::from("./file_credstore"); jail.set_env("BT_CREDSTORE_TYPE", "File"); jail.set_env("BT_CREDSTORE_PATH", expected.display()); let config = BtfsdConfig::new().unwrap(); let success = if let CredStoreConfig::File { path: actual } = config.cred_store { expected == actual } else { false }; assert!(success); Ok(()) }) } #[test] fn cred_store_tpm_path_tabrmd() { Jail::expect_with(|jail| { let expected_path = PathBuf::from("./tpm_credstore"); let expected_tabrmd = String::from("bus_type=session"); jail.set_env("BT_CREDSTORE_TYPE", "Tpm"); jail.set_env("BT_CREDSTORE_PATH", expected_path.display()); jail.set_env("BT_CREDSTORE_TABRMD", expected_tabrmd.as_str()); let config = BtfsdConfig::new().unwrap(); let success = if let CredStoreConfig::Tpm { path: actual_path, tabrmd: actual_tabrmd, } = config.cred_store { expected_path == actual_path && expected_tabrmd == actual_tabrmd } else { false }; assert!(success); Ok(()) }) } #[test] fn ip_addr() { Jail::expect_with(|jail| { let expected = IpAddr::from([172, 0, 0, 1]); jail.set_env("BT_IPADDR", expected); let config = BtfsdConfig::new().unwrap(); assert_eq!(expected, config.ip_addr); Ok(()) }) } #[test] fn block_dir() { Jail::expect_with(|jail| { let expected = PathBuf::from("/tmp/blocks"); jail.set_env("BT_BLOCKDIR", expected.display()); let config = BtfsdConfig::new().unwrap(); assert_eq!(expected, config.block_dir); Ok(()) }) } #[test] fn console() { Jail::expect_with(|jail| { let expected_addr = SocketAddr::from(([127, 0, 0, 42], 4129)); let expected_dist_path = PathBuf::from("/var/www"); jail.set_env("BT_CONSOLE_ADDR", expected_addr); jail.set_env("BT_CONSOLE_DISTPATH", expected_dist_path.display()); let expected = BtConsoleConfig::new(expected_addr, expected_dist_path); let config = BtfsdConfig::new().unwrap(); assert_eq!(expected, config.console.unwrap()); Ok(()) }) } }