Selaa lähdekoodia

* Updated all of the binary crates to use btconfig. It seems like
there's still some work to do refining the configuration system.
* Modified the Dockerfile to be generic, allowing it to build images
for all of the binary crates.

Matthew Carr 1 vuosi sitten
vanhempi
commit
4c31710ee0

+ 7 - 0
Cargo.lock

@@ -233,15 +233,18 @@ name = "btfsd"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "btconfig",
  "btfproto",
  "btlib",
  "btlib-tests",
  "btmsg",
  "btserde",
+ "config",
  "ctor",
  "env_logger",
  "libc",
  "log",
+ "serde",
  "swtpm-harness",
  "tempdir",
  "tokio",
@@ -252,17 +255,20 @@ name = "btfuse"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "btconfig",
  "btfproto",
  "btfproto-tests",
  "btlib",
  "btmsg",
  "btserde",
+ "config",
  "ctor",
  "env_logger",
  "fuse-backend-rs",
  "futures",
  "libc",
  "log",
+ "serde",
  "serde_json",
  "swtpm-harness",
  "tempdir",
@@ -355,6 +361,7 @@ dependencies = [
 name = "btserde"
 version = "0.1.0"
 dependencies = [
+ "paste",
  "serde",
  "serde-big-array",
 ]

+ 54 - 137
crates/btconfig/src/lib.rs

@@ -1,17 +1,18 @@
 use btlib::{
-    self, bterr,
-    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore},
+    self,
+    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore, Creds},
+    error::BtErr,
     Result,
 };
 use config::{
-    builder::{AsyncState, DefaultState},
+    builder::{AsyncState, BuilderState, DefaultState},
     ConfigBuilder, Environment,
 };
-use serde::{de::Visitor, Deserialize, Deserializer};
-use std::{path::PathBuf, result::Result as StdResult, str::FromStr};
+use serde::Deserialize;
+use std::{path::PathBuf, sync::Arc};
 
 /// Attempts to unwrap the [Option] field in `$config` called `$name`. This macro internal
-/// uses the `?` operator, and can only be used in method which return a [Result] which has an
+/// uses the `?` operator, and can only be used in methods which return a [Result] which has an
 /// error type that a [btlib::Error] can be converted to.
 #[macro_export]
 macro_rules! get_setting {
@@ -36,63 +37,51 @@ fn environment_source() -> Environment {
     Environment::with_prefix("BT").separator("_")
 }
 
+fn set_defaults<St: BuilderState>(builder: ConfigBuilder<St>) -> Result<ConfigBuilder<St>> {
+    builder
+        .set_default(CredStoreCfg::TAG, "File")?
+        .set_default(CredStoreCfg::PATH, "./state/file_cred_store")
+        .bterr()
+}
+
 /// An extension to [ConfigBuilder] which allows it to be configured using Blocktree's default
 /// settings.
-pub trait ConfigBuilderExt {
+pub trait ConfigBuilderExt: Sized {
     /// Configure this [ConfigBuilder] with Blocktree's default settings.
-    fn btconfig(self) -> Self;
+    fn btconfig(self) -> Result<Self>;
 }
 
 impl ConfigBuilderExt for ConfigBuilder<DefaultState> {
-    fn btconfig(self) -> Self {
-        self.add_source(environment_source())
+    fn btconfig(self) -> Result<Self> {
+        let builder = set_defaults(self)?.add_source(environment_source());
+        Ok(builder)
     }
 }
 
 impl ConfigBuilderExt for ConfigBuilder<AsyncState> {
-    fn btconfig(self) -> Self {
-        self.add_source(environment_source())
+    fn btconfig(self) -> Result<Self> {
+        let builder = set_defaults(self)?.add_source(environment_source());
+        Ok(builder)
     }
 }
 
-/// A struct which assists with very simple parsing of a configuration string.
-struct StrParser<'a>(&'a str);
-
-impl<'a> StrParser<'a> {
-    /// Assert that the given token occurs at the current position in the input `str`.
-    fn assert(&mut self, token: &str) -> btlib::Result<()> {
-        if self.0.starts_with(token) {
-            self.0 = &self.0[token.len()..];
-            Ok(())
-        } else {
-            Err(bterr!("string does not start with {token}"))
-        }
-    }
-
-    /// Consumes all characters up to `stop`, and returns them. Note that the returned `str` does
-    /// not contain `stop`, and the current position in the `str` is set to the next character after
-    /// `stop`. If `stop` is not encountered, then the remaining `str` is returned.
-    fn consume_up_to(&mut self, stop: char) -> &str {
-        let mut count = 0;
-        for c in self.0.chars() {
-            if c == stop {
-                break;
-            }
-            count += 1;
-        }
-        let output = &self.0[..count];
-        let new_start = self.0.len().min(count + 1);
-        self.0 = &self.0[new_start..];
-        output
-    }
-
-    /// Returns the remaining unconsumed part of the `str`.
-    fn remaining(&self) -> &str {
-        self.0
+#[macro_export]
+macro_rules! config_keys {
+    {
+        const $struct_key_name:ident = $struct_key:literal;
+        $(const $field_key_name:ident = $field_key:literal;)+
+    } => {
+        #[allow(dead_code)]
+        const $struct_key_name: &str = $struct_key;
+        $(
+            #[allow(dead_code)]
+            const $field_key_name: &str = concat!($struct_key, ".", $field_key);
+        )+
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Deserialize)]
+#[serde(tag = "type")]
 /// Configuration which specifies how to construct a [btlib::crypto::CredStore].
 pub enum CredStoreCfg {
     /// Specifies that a [btlib::crypto::file_cred_store::FileCredStore] should be used.
@@ -103,12 +92,21 @@ pub enum CredStoreCfg {
     /// Specifies that a [btlib::crypto::tpm::TpmCredStore] should be used.
     Tpm {
         /// The state path to use for the TPM credential store.
-        tpm_state_path: PathBuf,
+        path: PathBuf,
         /// The configuration string to pass to `tss-esapi`.
         tabrmd: String,
     },
 }
 
+impl CredStoreCfg {
+    config_keys! {
+        const STRUCT_KEY = "credstore";
+        const TAG = "type";
+        const PATH = "path";
+        const TABRMD = "tabrmd";
+    }
+}
+
 pub trait CredStoreConsumer {
     type Output;
     fn consume<C: CredStore>(self, cred_store: C) -> Self::Output;
@@ -123,7 +121,7 @@ impl CredStoreCfg {
                 Ok(consumer.consume(store))
             }
             CredStoreCfg::Tpm {
-                tpm_state_path,
+                path: tpm_state_path,
                 tabrmd,
             } => {
                 let store = TpmCredStore::from_tabrmd(&tabrmd, tpm_state_path)?;
@@ -133,94 +131,13 @@ impl CredStoreCfg {
     }
 }
 
-impl FromStr for CredStoreCfg {
-    type Err = btlib::Error;
-    fn from_str(value: &str) -> StdResult<Self, Self::Err> {
-        let mut parser = StrParser(value);
-        parser.assert("kind=")?;
-        let kind = parser.consume_up_to(',');
-        match kind {
-            "file" => {
-                parser.assert("path=")?;
-                let path = PathBuf::from(parser.remaining());
-                Ok(CredStoreCfg::File { path })
-            }
-            "tpm" => {
-                parser.assert("tpm_state_path=")?;
-                let tpm_state_path = PathBuf::from(parser.consume_up_to(','));
-                let tabrmd = parser.remaining().to_string();
-                Ok(CredStoreCfg::Tpm {
-                    tpm_state_path,
-                    tabrmd,
-                })
-            }
-            _ => Err(bterr!(
-                "unrecognized CredStore kind (expected 'file' or 'tpm'): {kind}"
-            )),
-        }
-    }
-}
-
-impl<'de> Deserialize<'de> for CredStoreCfg {
-    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
-        struct StructVisitor;
-
-        impl<'a> Visitor<'a> for StructVisitor {
-            type Value = CredStoreCfg;
-
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                write!(formatter, "cred store configuration string")
-            }
-
-            fn visit_str<E: serde::de::Error>(self, v: &str) -> StdResult<Self::Value, E> {
-                CredStoreCfg::from_str(v).map_err(serde::de::Error::custom)
-            }
-        }
-
-        deserializer.deserialize_str(StructVisitor)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use serde::de::value::StrDeserializer;
-
-    use super::*;
-
-    #[test]
-    fn deserialize_file_cred_store_config() {
-        const PATH: &str = "./state/cred_store";
-        let input = format!("kind=file,path={PATH}");
-        let deserializer = StrDeserializer::<'_, serde::de::value::Error>::new(input.as_str());
-
-        let actual = CredStoreCfg::deserialize(deserializer).unwrap();
-
-        let passed = if let CredStoreCfg::File { path } = actual {
-            path.to_str().unwrap() == PATH
-        } else {
-            false
-        };
-        assert!(passed);
-    }
-
-    #[test]
-    fn deserialize_tpm_cred_store_config() {
-        const STATE_PATH: &str = "./state/cred_store";
-        const TABRMD: &str = "bus_type=session";
-        let input = format!("kind=tpm,tpm_state_path={STATE_PATH},{TABRMD}");
-        let deserializer = StrDeserializer::<'_, serde::de::value::Error>::new(input.as_str());
-
-        let actual = CredStoreCfg::deserialize(deserializer).unwrap();
+/// A [CredStoreConsumer] which gets the node creds from the [CredStore], then returns them
+/// as an `Arc<dyn Creds>`.
+pub struct NodeCredConsumer;
 
-        let passed = if let CredStoreCfg::Tpm {
-            tpm_state_path,
-            tabrmd,
-        } = actual
-        {
-            tpm_state_path.to_str().unwrap() == STATE_PATH && &tabrmd == TABRMD
-        } else {
-            false
-        };
-        assert!(passed);
+impl CredStoreConsumer for NodeCredConsumer {
+    type Output = Result<Arc<dyn Creds>>;
+    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+        Ok(Arc::new(cred_store.node_creds()?))
     }
 }

+ 3 - 0
crates/btfsd/Cargo.toml

@@ -9,11 +9,14 @@ edition = "2021"
 btlib = { path = "../btlib" }
 btmsg = { path = "../btmsg" }
 btfproto = { path = "../btfproto" }
+btconfig = { path = "../btconfig" }
 tokio = { version = "1.24.2", features = ["rt", "rt-multi-thread", "time"] }
 log = "0.4.17"
 env_logger = "0.9.0"
 btlib-tests = { path = "../btlib-tests" }
 anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+serde = { version = "^1.0.136", features = ["derive"] }
+config = "0.13.3"
 
 [dev-dependencies]
 swtpm-harness = { path = "../swtpm-harness" }

+ 0 - 10
crates/btfsd/btfsd_init.sh

@@ -1,10 +0,0 @@
-#!/bin/sh
-# Init script to run in the btfsd container.
-set -e
-
-if [ "$BTFSD_USESWTPM" = 'true' ]; then
-    export DBUS_SESSION_BUS_ADDRESS="$(dbus-daemon --fork --session --print-address)"
-fi
-
-cd app
-./btfsd "$@"

+ 0 - 149
crates/btfsd/src/config.rs

@@ -1,149 +0,0 @@
-use btlib::bterr;
-
-// SPDX-License-Identifier: AGPL-3.0-or-later
-use super::DEFAULT_CONFIG;
-use std::{net::IpAddr, path::PathBuf, str::FromStr};
-
-struct StrParser<'a>(&'a str);
-
-impl<'a> StrParser<'a> {
-    fn assert(&mut self, token: &str) -> btlib::Result<()> {
-        if self.0.starts_with(token) {
-            self.0 = &self.0[token.len()..];
-            Ok(())
-        } else {
-            Err(bterr!("string does not start with {token}"))
-        }
-    }
-
-    fn consume_up_to(&mut self, stop: char) -> btlib::Result<&str> {
-        let mut count = 0;
-        for c in self.0.chars() {
-            if c == stop {
-                break;
-            }
-            count += 1;
-        }
-        let output = &self.0[..count];
-        let new_start = self.0.len().min(count + 1);
-        self.0 = &self.0[new_start..];
-        Ok(output)
-    }
-
-    fn remaining(&self) -> &str {
-        self.0
-    }
-}
-
-pub enum CredStoreCfg {
-    File {
-        path: PathBuf,
-    },
-    Tpm {
-        tpm_state_path: PathBuf,
-        tabrmd: String,
-    },
-}
-
-impl FromStr for CredStoreCfg {
-    type Err = btlib::Error;
-    fn from_str(value: &str) -> Result<Self, Self::Err> {
-        let mut parser = StrParser(value);
-        parser.assert("kind=")?;
-        let kind = parser.consume_up_to(',')?;
-        match kind {
-            "file" => {
-                parser.assert("path=")?;
-                let path = PathBuf::from(parser.remaining());
-                Ok(CredStoreCfg::File { path })
-            }
-            "tpm" => {
-                parser.assert("tpm_state_path=")?;
-                let tpm_state_path = PathBuf::from(parser.consume_up_to(',')?);
-                let tabrmd = parser.remaining().to_string();
-                Ok(CredStoreCfg::Tpm {
-                    tpm_state_path,
-                    tabrmd,
-                })
-            }
-            _ => Err(bterr!(
-                "unrecognized CredStore kind (expected 'file' or 'tpm'): {kind}"
-            )),
-        }
-    }
-}
-
-pub struct Config {
-    pub cred_store_cfg: CredStoreCfg,
-    pub ip_addr: IpAddr,
-    pub block_dir: PathBuf,
-}
-
-impl Config {
-    pub fn builder() -> ConfigBuilder {
-        ConfigBuilder::new()
-    }
-}
-
-trait OptionExt<'a> {
-    fn str_unwrap_or(self, default: &'a str) -> &'a str;
-}
-
-impl<'a> OptionExt<'a> for &'a Option<String> {
-    fn str_unwrap_or(self, default: &'a str) -> &'a str {
-        self.as_ref().map(|e| e.as_str()).unwrap_or(default)
-    }
-}
-
-#[derive(Default)]
-pub struct ConfigBuilder {
-    pub cred_store: Option<String>,
-    pub ip_addr: Option<String>,
-    pub block_dir: Option<String>,
-}
-
-impl ConfigBuilder {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn with_cred_store(mut self, cred_store: Option<String>) -> Self {
-        self.cred_store = cred_store;
-        self
-    }
-
-    pub fn with_ip_addr(mut self, ip_addr: Option<String>) -> Self {
-        self.ip_addr = ip_addr;
-        self
-    }
-
-    pub fn with_block_dir(mut self, block_dir: Option<String>) -> Self {
-        self.block_dir = block_dir;
-        self
-    }
-
-    pub fn build(self) -> Config {
-        let cred_store_cfg =
-            CredStoreCfg::from_str(self.cred_store.str_unwrap_or(DEFAULT_CONFIG.cred_store))
-                .unwrap();
-        let ip_addr = IpAddr::from_str(self.ip_addr.str_unwrap_or(DEFAULT_CONFIG.ip_addr)).unwrap();
-        let block_dir = PathBuf::from(self.block_dir.str_unwrap_or(DEFAULT_CONFIG.block_dir));
-        Config {
-            cred_store_cfg,
-            ip_addr,
-            block_dir,
-        }
-    }
-}
-
-pub struct ConfigRef<'a> {
-    pub cred_store: &'a str,
-    pub ip_addr: &'a str,
-    pub block_dir: &'a str,
-}
-
-pub struct Envvars<'a> {
-    pub cred_store: &'a str,
-    pub ip_addr: &'a str,
-    pub block_dir: &'a str,
-}

+ 55 - 65
crates/btfsd/src/main.rs

@@ -1,71 +1,58 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
-mod config;
-
+use ::config::Config;
+use btconfig::{ConfigBuilderExt, CredStoreCfg, NodeCredConsumer};
 use btfproto::{
     local_fs::{LocalFs, ModeAuthorizer},
     server::{new_fs_server, FsProvider},
 };
-use btlib::{
-    config_helpers::from_envvar,
-    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore, Creds},
-    log::BuilderExt,
-};
+use btlib::{crypto::Creds, error::DisplayErr, log::BuilderExt, Result};
 use btmsg::Receiver;
-use config::{Config, ConfigRef, CredStoreCfg, Envvars};
-use std::{path::PathBuf, sync::Arc};
+use std::{net::IpAddr, path::PathBuf, sync::Arc};
 
-const ENVVARS: Envvars<'static> = Envvars {
-    cred_store: "BTFSD_CREDSTORE",
-    ip_addr: "BTFSD_IPADDR",
-    block_dir: "BTFSD_BLOCKDIR",
-};
+use serde::Deserialize;
 
-const DEFAULT_CONFIG: ConfigRef<'static> = ConfigRef {
-    cred_store: "kind=tpm,tpm_state_file=./state/tpm_state,bus_type=session",
-    ip_addr: "127.0.0.1",
-    block_dir: "./state/bt",
-};
+#[derive(Debug, Deserialize, Clone)]
+struct AppConfig {
+    credstore: CredStoreCfg,
+    ipaddr: IpAddr,
+    blockdir: PathBuf,
+}
 
-async fn provider(block_dir: PathBuf, creds: Arc<dyn Creds>) -> impl FsProvider {
+impl AppConfig {
+    fn new() -> Result<AppConfig> {
+        Config::builder()
+            .set_default("ipaddr", "127.0.0.1")?
+            .set_default("blockdir", "./state/blocks")?
+            .btconfig()?
+            .build()?
+            .try_deserialize()
+            .map_err(|err| err.into())
+    }
+}
+
+async fn provider(block_dir: PathBuf, creds: Arc<dyn Creds>) -> Result<impl FsProvider> {
     if block_dir.exists() {
-        LocalFs::new_existing(block_dir, creds, ModeAuthorizer).unwrap()
+        LocalFs::new_existing(block_dir, creds, ModeAuthorizer)
     } else {
-        std::fs::create_dir_all(&block_dir).unwrap();
-        LocalFs::new_empty(block_dir, 0, creds, ModeAuthorizer)
-            .await
-            .unwrap()
+        std::fs::create_dir_all(&block_dir)?;
+        LocalFs::new_empty(block_dir, 0, creds, ModeAuthorizer).await
     }
 }
 
-async fn receiver(config: Config) -> impl Receiver {
-    let node_creds: Arc<dyn Creds> = match config.cred_store_cfg {
-        CredStoreCfg::File { path } => {
-            let cred_store = FileCredStore::new(path).unwrap();
-            cred_store.node_creds().unwrap()
-        }
-        CredStoreCfg::Tpm {
-            tpm_state_path,
-            tabrmd,
-        } => {
-            let cred_store = TpmCredStore::from_tabrmd(&tabrmd, tpm_state_path).unwrap();
-            Arc::new(cred_store.node_creds().unwrap())
-        }
-    };
-    let provider = Arc::new(provider(config.block_dir, node_creds.clone()).await);
-    new_fs_server(config.ip_addr, Arc::new(node_creds), provider).unwrap()
+async fn receiver(config: AppConfig) -> Result<impl Receiver> {
+    let node_creds = config.credstore.consume(NodeCredConsumer)??;
+    let provider = Arc::new(provider(config.blockdir, node_creds.clone()).await?);
+    new_fs_server(config.ipaddr, Arc::new(node_creds), provider)
 }
 
 #[tokio::main]
-async fn main() {
+async fn main() -> Result<()> {
     env_logger::Builder::from_default_env().btformat().init();
-    let config = Config::builder()
-        .with_cred_store(from_envvar(ENVVARS.cred_store).unwrap())
-        .with_ip_addr(from_envvar(ENVVARS.ip_addr).unwrap())
-        .with_block_dir(from_envvar(ENVVARS.block_dir).unwrap())
-        .build();
-    let receiver = receiver(config).await;
+    let config = AppConfig::new()?;
+    let receiver = receiver(config).await?;
     log::debug!("ready to accept connections");
-    receiver.complete().unwrap().await.unwrap();
+    receiver.complete()?.await.display_err()?;
+    Ok(())
 }
 
 #[cfg(test)]
@@ -74,7 +61,10 @@ mod tests {
 
     use btfproto::{client::FsClient, msg::*};
     use btlib::{
-        crypto::{ConcreteCreds, CredsPriv, CredsPub},
+        crypto::{
+            file_cred_store::FileCredStore, tpm::TpmCredStore, ConcreteCreds, CredStore, CredsPriv,
+            CredsPub,
+        },
         log::BuilderExt,
         AuthzAttrs, BlockMetaSecrets, Epoch, IssuedProcRec, Principaled, ProcRec,
     };
@@ -102,10 +92,10 @@ mod tests {
 
     async fn file_test_case(
         dir: TempDir,
-        ip_addr: IpAddr,
+        ipaddr: IpAddr,
     ) -> FileTestCase<impl Receiver, impl Transmitter> {
         let file_store_path = dir.path().join("cred_store");
-        let cred_store_cfg = CredStoreCfg::File {
+        let credstore = CredStoreCfg::File {
             path: file_store_path.clone(),
         };
 
@@ -113,12 +103,12 @@ mod tests {
             let cred_store = FileCredStore::new(file_store_path).unwrap();
             cred_store.provision(ROOT_PASSWD).unwrap();
         }
-        let config = Config {
-            cred_store_cfg,
-            ip_addr,
-            block_dir: dir.path().join(BT_DIR),
+        let config = AppConfig {
+            credstore,
+            ipaddr,
+            blockdir: dir.path().join(BT_DIR),
         };
-        let rx = receiver(config).await;
+        let rx = receiver(config).await.unwrap();
         let tx = rx.transmitter(rx.addr().clone()).await.unwrap();
         let client = FsClient::new(tx);
         FileTestCase {
@@ -147,19 +137,19 @@ mod tests {
     async fn tpm_test_case(
         dir: TempDir,
         harness: TpmCredStoreHarness,
-        ip_addr: IpAddr,
+        ipaddr: IpAddr,
     ) -> TestCase<impl Receiver, impl Transmitter> {
         let swtpm = harness.swtpm();
-        let cred_store_cfg = CredStoreCfg::Tpm {
-            tpm_state_path: swtpm.state_path().to_owned(),
+        let credstore = CredStoreCfg::Tpm {
+            path: swtpm.state_path().to_owned(),
             tabrmd: swtpm.tabrmd_config().to_owned(),
         };
-        let config = Config {
-            cred_store_cfg,
-            ip_addr,
-            block_dir: dir.path().join(BT_DIR),
+        let config = AppConfig {
+            credstore,
+            ipaddr,
+            blockdir: dir.path().join(BT_DIR),
         };
-        let rx = receiver(config).await;
+        let rx = receiver(config).await.unwrap();
         let tx = rx.transmitter(rx.addr().clone()).await.unwrap();
         let client = FsClient::new(tx);
         TestCase {

+ 3 - 0
crates/btfuse/Cargo.toml

@@ -12,6 +12,7 @@ btserde = { path = "../btserde" }
 swtpm-harness = { path = "../swtpm-harness" }
 btfproto = { path = "../btfproto" }
 btmsg = { path = "../btmsg" }
+btconfig = { path = "../btconfig" }
 tokio = { version = "1.24.2", features = ["rt", "rt-multi-thread"] }
 fuse-backend-rs = { version = "0.9.6", features = ["async-io"] }
 log = "0.4.17"
@@ -20,6 +21,8 @@ anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
 libc = { version = "0.2.137" }
 serde_json = "1.0.92"
 futures = "0.3.25"
+serde = { version = "^1.0.136", features = ["derive"] }
+config = "0.13.3"
 
 [dev-dependencies]
 btfproto-tests = { path = "../btfproto-tests" }

+ 0 - 152
crates/btfuse/src/config.rs

@@ -1,152 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0-or-later
-use super::DEFAULT_CONFIG;
-
-use btlib::BlockPath;
-use btmsg::BlockAddr;
-
-use std::{
-    net::IpAddr,
-    num::NonZeroUsize,
-    path::{Path, PathBuf},
-    sync::Arc,
-    thread::available_parallelism,
-};
-
-#[derive(PartialEq, Eq, Clone)]
-pub enum FsKind {
-    Local(PathBuf),
-    Remote(BlockAddr),
-}
-
-#[derive(PartialEq, Eq, Clone)]
-pub struct Config {
-    pub fs_kind: FsKind,
-    pub mnt_dir: PathBuf,
-    pub tpm_state_file: PathBuf,
-    pub tabrmd: String,
-    pub mnt_options: String,
-    pub threads: NonZeroUsize,
-}
-
-impl Config {
-    pub fn builder() -> ConfigBuilder {
-        ConfigBuilder::new()
-    }
-}
-
-pub struct ConfigRef<'a> {
-    pub block_dir: &'a str,
-    pub mnt_dir: &'a str,
-    pub tpm_state_file: &'a str,
-    pub tabrmd: &'a str,
-    pub mnt_options: &'a str,
-    pub threads: Option<NonZeroUsize>,
-}
-
-#[derive(Default, PartialEq, Eq, Clone)]
-pub struct ConfigBuilder {
-    pub block_dir: Option<PathBuf>,
-    pub mnt_dir: Option<PathBuf>,
-    pub remote_ip: Option<String>,
-    pub remote_path: Option<String>,
-    pub tpm_state_file: Option<PathBuf>,
-    pub tabrmd: Option<String>,
-    pub mnt_options: Option<String>,
-    pub threads: Option<NonZeroUsize>,
-}
-
-impl ConfigBuilder {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn with_block_dir(mut self, block_dir: Option<PathBuf>) -> Self {
-        self.block_dir = block_dir;
-        self
-    }
-
-    pub fn with_mnt_dir(mut self, mnt_dir: Option<PathBuf>) -> Self {
-        self.mnt_dir = mnt_dir;
-        self
-    }
-
-    #[allow(dead_code)]
-    pub fn with_tpm_state_file(mut self, mnt_dir: Option<PathBuf>) -> Self {
-        self.tpm_state_file = mnt_dir;
-        self
-    }
-
-    pub fn with_tabrmd(mut self, tabrmd: Option<String>) -> Self {
-        self.tabrmd = tabrmd;
-        self
-    }
-
-    pub fn with_mnt_options(mut self, mnt_options: Option<String>) -> Self {
-        self.mnt_options = mnt_options;
-        self
-    }
-
-    pub fn with_remote_ip(mut self, remote_ip: Option<String>) -> Self {
-        self.remote_ip = remote_ip;
-        self
-    }
-
-    pub fn with_remote_path(mut self, remote_path: Option<String>) -> Self {
-        self.remote_path = remote_path;
-        self
-    }
-
-    #[allow(dead_code)]
-    pub fn with_threads(mut self, threads: Option<NonZeroUsize>) -> Self {
-        self.threads = threads;
-        self
-    }
-
-    pub fn build(self) -> Config {
-        let remote_addr = self
-            .remote_ip
-            .zip(self.remote_path)
-            .map(|(ip_str, path_str)| {
-                let ip: IpAddr = ip_str.parse().unwrap();
-                let path = BlockPath::try_from(path_str.as_str()).unwrap();
-                BlockAddr::new(ip, Arc::new(path))
-            });
-        let fs_kind = if let Some(remote_addr) = remote_addr {
-            FsKind::Remote(remote_addr)
-        } else {
-            let block_dir = self
-                .block_dir
-                .unwrap_or_else(|| Path::new(DEFAULT_CONFIG.block_dir).to_owned());
-            FsKind::Local(block_dir)
-        };
-        Config {
-            fs_kind,
-            mnt_dir: self
-                .mnt_dir
-                .unwrap_or_else(|| Path::new(DEFAULT_CONFIG.mnt_dir).to_owned()),
-            tpm_state_file: self
-                .tpm_state_file
-                .unwrap_or_else(|| Path::new(DEFAULT_CONFIG.tpm_state_file).to_owned()),
-            tabrmd: self
-                .tabrmd
-                .unwrap_or_else(|| DEFAULT_CONFIG.tabrmd.to_owned()),
-            mnt_options: self
-                .mnt_options
-                .unwrap_or_else(|| DEFAULT_CONFIG.mnt_options.to_owned()),
-            threads: self.threads.unwrap_or_else(|| {
-                DEFAULT_CONFIG.threads.unwrap_or_else(|| {
-                    let threads = available_parallelism().unwrap().get() / 2;
-                    NonZeroUsize::new(threads.max(1)).unwrap()
-                })
-            }),
-        }
-    }
-}
-
-pub struct EnvVars {
-    pub block_dir: &'static str,
-    pub tabrmd: &'static str,
-    pub mnt_options: &'static str,
-    pub remote_ip: &'static str,
-    pub remote_path: &'static str,
-}

+ 23 - 9
crates/btfuse/src/fuse_daemon.rs

@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
-use crate::{fuse_fs::FuseFs, PathExt, DEFAULT_CONFIG};
+use crate::{fuse_fs::FuseFs, PathExt};
 
 use btfproto::server::FsProvider;
 use btlib::{bterr, BlockPath, Result};
@@ -34,11 +34,19 @@ mod private {
 
     /// Calls into libfuse3 to mount this file system at the given path. The file descriptor to use
     /// to communicate with the kernel is returned.
-    fn mount_at<P: AsRef<Path>>(mnt_point: P) -> File {
-        let mountpoint = CString::new(mnt_point.as_ref().as_os_str().as_bytes()).unwrap();
-        let options = CString::new(DEFAULT_CONFIG.mnt_options).unwrap();
+    fn mount_at<P: AsRef<Path>>(mnt_point: P, mnt_options: &str) -> Result<File> {
+        let mountpoint = CString::new(mnt_point.as_ref().as_os_str().as_bytes())?;
+        let options = CString::new(mnt_options)?;
+        // Safety: mountpoint and options are both valid C strings.
         let raw_fd = unsafe { fuse_open_channel(mountpoint.as_ptr(), options.as_ptr()) };
-        unsafe { File::from_raw_fd(raw_fd) }
+        // According to the fuse3 docs only -1 will be returned on error.
+        // source: http://libfuse.github.io/doxygen/fuse_8h.html#a9e8c9af40b22631f9f2636019cd073b6
+        if raw_fd < 0 {
+            return Err(bterr!("an error occurred in fuse_open_channel"));
+        }
+        // Safety: raw_rd is positive, indicating that fuse_open_channel succeeded.
+        let file = unsafe { File::from_raw_fd(raw_fd) };
+        Ok(file)
     }
 
     pub struct FuseDaemon {
@@ -52,18 +60,24 @@ mod private {
 
         pub fn new<P: 'static + FsProvider>(
             mnt_path: PathBuf,
-            num_tasks: NonZeroUsize,
+            mnt_options: &str,
+            num_tasks: Option<NonZeroUsize>,
             fallback_path: Arc<BlockPath>,
             mounted_signal: Option<oneshot::Sender<()>>,
             provider: P,
         ) -> Result<Self> {
             let server = Arc::new(Server::new(FuseFs::new(provider, fallback_path)));
-            let session = Self::session(mnt_path)?;
+            let session = Self::session(mnt_path, mnt_options)?;
             if let Some(tx) = mounted_signal {
                 tx.send(())
                     .map_err(|_| bterr!("failed to send mounted signal"))?;
             }
             let mut set = JoinSet::new();
+            let num_tasks = if let Some(num_tasks) = num_tasks {
+                num_tasks
+            } else {
+                std::thread::available_parallelism()?
+            };
             log::debug!("spawning {num_tasks} blocking tasks");
             for task_num in 0..num_tasks.get() {
                 let server = server.clone();
@@ -81,11 +95,11 @@ mod private {
             Ok(Self { set, session })
         }
 
-        fn session<T: AsRef<Path>>(mnt_path: T) -> Result<FuseSession> {
+        fn session<T: AsRef<Path>>(mnt_path: T, mnt_options: &str) -> Result<FuseSession> {
             mnt_path.try_create_dir()?;
             let mut session =
                 FuseSession::new(mnt_path.as_ref(), Self::FSNAME, Self::FSTYPE, false)?;
-            session.set_fuse_file(mount_at(mnt_path));
+            session.set_fuse_file(mount_at(mnt_path, mnt_options)?);
             Ok(session)
         }
 

+ 77 - 61
crates/btfuse/src/main.rs

@@ -1,45 +1,65 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
 mod fuse_daemon;
+use btconfig::{config_keys, ConfigBuilderExt, CredStoreCfg, NodeCredConsumer};
 use fuse_daemon::FuseDaemon;
-mod config;
 mod fuse_fs;
 
-use config::{Config, ConfigRef, EnvVars, FsKind};
-
+use ::config::Config as ExtConfig;
 use btfproto::{client::FsClient, local_fs::LocalFs, server::FsProvider};
 use btlib::{
-    config_helpers::from_envvar,
-    crypto::{
-        tpm::{TpmCredStore, TpmCreds},
-        CredStore, Creds, CredsPriv,
-    },
+    crypto::{Creds, CredsPriv},
+    error::BtErr,
     Result,
 };
 use btmsg::{transmitter, BlockAddr};
+use serde::Deserialize;
 use std::{
     fs::{self},
     io,
+    num::NonZeroUsize,
     path::{Path, PathBuf},
     sync::Arc,
 };
 use tokio::sync::oneshot;
 
-const ENVVARS: EnvVars = EnvVars {
-    block_dir: "BT_BLOCKDIR",
-    tabrmd: "BT_TABRMD",
-    mnt_options: "BT_MNTOPTS",
-    remote_ip: "BT_REMOTEIP",
-    remote_path: "BT_REMOTEPATH",
-};
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(tag = "type")]
+pub enum FsKind {
+    Local { path: PathBuf },
+    Remote { addr: BlockAddr },
+}
 
-const DEFAULT_CONFIG: ConfigRef<'static> = ConfigRef {
-    block_dir: "./bt",
-    mnt_dir: "./mnt",
-    tpm_state_file: "./tpm_state",
-    tabrmd: "bus_type=session",
-    mnt_options: "default_permissions",
-    threads: None,
-};
+impl FsKind {
+    config_keys! {
+        const STRUCT_KEY = "fskind";
+        const TAG = "type";
+        const PATH = "path";
+        const ADDR = "addr";
+    }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct AppConfig {
+    credstore: CredStoreCfg,
+    fskind: FsKind,
+    mntdir: PathBuf,
+    mntoptions: String,
+    threads: Option<NonZeroUsize>,
+}
+
+impl AppConfig {
+    fn new() -> Result<Self> {
+        ExtConfig::builder()
+            .set_default(FsKind::TAG, "Local")?
+            .set_default(FsKind::PATH, "./state/blocks")?
+            .set_default("mntdir", "./state/mnt")?
+            .set_default("mntoptions", "default_permissions")?
+            .btconfig()?
+            .build()?
+            .try_deserialize()
+            .bterr()
+    }
+}
 
 trait PathExt {
     fn try_create_dir(&self) -> io::Result<()>;
@@ -57,11 +77,6 @@ impl<T: AsRef<Path>> PathExt for T {
     }
 }
 
-fn node_creds(state_file: PathBuf, tabrmd_cfg: &str) -> Result<TpmCreds> {
-    let cred_store = TpmCredStore::from_tabrmd(tabrmd_cfg, state_file)?;
-    cred_store.node_creds()
-}
-
 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();
@@ -81,10 +96,8 @@ async fn remote_provider<C: 'static + Creds + Send + Sync>(
     Ok(client)
 }
 
-async fn run_daemon(config: Config, mounted_signal: Option<oneshot::Sender<()>>) {
-    let node_creds = Arc::new(
-        node_creds(config.tpm_state_file, &config.tabrmd).expect("failed to get node creds"),
-    );
+async fn run_daemon(config: AppConfig, mounted_signal: Option<oneshot::Sender<()>>) {
+    let node_creds = config.credstore.consume(NodeCredConsumer).unwrap().unwrap();
     let fallback_path = {
         let writecap = node_creds
             .writecap()
@@ -92,21 +105,22 @@ async fn run_daemon(config: Config, mounted_signal: Option<oneshot::Sender<()>>)
             .unwrap();
         Arc::new(writecap.bind_path())
     };
-    let mut daemon = match config.fs_kind {
-        FsKind::Local(btdir) => {
+    let mut daemon = match config.fskind {
+        FsKind::Local { path: btdir } => {
             log::info!("starting daemon with local provider using {:?}", btdir);
             let provider = local_provider(btdir, node_creds)
                 .await
                 .expect("failed to create local provider");
             FuseDaemon::new(
-                config.mnt_dir,
+                config.mntdir,
+                &config.mntoptions,
                 config.threads,
                 fallback_path,
                 mounted_signal,
                 provider,
             )
         }
-        FsKind::Remote(remote_addr) => {
+        FsKind::Remote { addr: remote_addr } => {
             log::info!(
                 "starting daemon with remote provider using {:?}",
                 remote_addr.socket_addr()
@@ -115,7 +129,8 @@ async fn run_daemon(config: Config, mounted_signal: Option<oneshot::Sender<()>>)
                 .await
                 .expect("failed to create remote provider");
             FuseDaemon::new(
-                config.mnt_dir,
+                config.mntdir,
+                &config.mntoptions,
                 config.threads,
                 fallback_path,
                 mounted_signal,
@@ -131,15 +146,7 @@ async fn run_daemon(config: Config, mounted_signal: Option<oneshot::Sender<()>>)
 #[tokio::main]
 async fn main() {
     env_logger::init();
-    let mut args = std::env::args_os().skip(1).map(PathBuf::from);
-    let builder = Config::builder()
-        .with_block_dir(from_envvar(ENVVARS.block_dir).unwrap().map(PathBuf::from))
-        .with_remote_ip(from_envvar(ENVVARS.remote_ip).unwrap())
-        .with_remote_path(from_envvar(ENVVARS.remote_path).unwrap())
-        .with_mnt_dir(args.next())
-        .with_tabrmd(from_envvar(ENVVARS.tabrmd).unwrap())
-        .with_mnt_options(from_envvar(ENVVARS.mnt_options).unwrap());
-    let config = builder.build();
+    let config = AppConfig::new().unwrap();
     run_daemon(config, None).await;
 }
 
@@ -148,7 +155,11 @@ mod test {
     use super::*;
 
     use btfproto::{local_fs::ModeAuthorizer, server::new_fs_server};
-    use btlib::{crypto::Creds, log::BuilderExt, Epoch, Principaled};
+    use btlib::{
+        crypto::{tpm::TpmCredStore, CredStore, Creds},
+        log::BuilderExt,
+        Epoch, Principaled,
+    };
     use btmsg::Receiver;
     use ctor::ctor;
     use std::{
@@ -242,7 +253,7 @@ mod test {
     const ROOT_PASSWD: &str = "password";
 
     struct TestCase<R: Receiver> {
-        config: Config,
+        config: AppConfig,
         handle: Option<JoinHandle<()>>,
         node_principal: OsString,
         stop_flag: Option<()>,
@@ -265,13 +276,8 @@ mod test {
         let tmp = TempDir::new("btfuse").unwrap();
         let (mounted_tx, mounted_rx) = oneshot::channel();
         let (swtpm, cred_store) = swtpm();
-        let builder = Config::builder()
-            .with_threads(Some(NonZeroUsize::new(1).unwrap()))
-            .with_mnt_dir(Some(tmp.path().join("mnt")))
-            .with_tpm_state_file(Some(swtpm.state_path().to_owned().into()))
-            .with_tabrmd(Some(swtpm.tabrmd_config().to_owned()));
         let block_dir = tmp.path().join("bt");
-        let (config, receiver) = if remote {
+        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();
@@ -280,12 +286,22 @@ mod test {
                 .unwrap();
             let ip_addr = IpAddr::V6(Ipv6Addr::LOCALHOST);
             let receiver = new_fs_server(ip_addr, node_creds.clone(), Arc::new(local_fs)).unwrap();
-            let builder = builder
-                .with_remote_ip(Some(ip_addr.to_string()))
-                .with_remote_path(Some(bind_path.to_string()));
-            (builder.build(), Some(receiver))
+            let fs_kind = FsKind::Remote {
+                addr: BlockAddr::new(ip_addr, Arc::new(bind_path)),
+            };
+            (fs_kind, Some(receiver))
         } else {
-            (builder.with_block_dir(Some(block_dir)).build(), None)
+            (FsKind::Local { path: block_dir }, None)
+        };
+        let config = AppConfig {
+            threads: Some(NonZeroUsize::new(1).unwrap()),
+            mntdir: tmp.path().join("mnt"),
+            credstore: CredStoreCfg::Tpm {
+                path: swtpm.state_path().to_owned().into(),
+                tabrmd: swtpm.tabrmd_config().to_owned(),
+            },
+            fskind: fs_kind,
+            mntoptions: "default_permissions".to_string(),
         };
         let config_clone = config.clone();
         let handle = tokio::spawn(async move {
@@ -334,13 +350,13 @@ mod test {
 
     impl<R: Receiver> TestCase<R> {
         fn mnt_dir(&self) -> &Path {
-            &self.config.mnt_dir
+            &self.config.mntdir
         }
 
         /// Signals to the daemon that it must stop.
         fn signal_stop(&mut self) {
             if let Some(_) = self.stop_flag.take() {
-                unmount(&self.config.mnt_dir)
+                unmount(&self.config.mntdir)
             }
         }
 

+ 2 - 2
crates/btlib/src/crypto.rs

@@ -2337,9 +2337,9 @@ impl<C: ?Sized + CredsPriv + CredsPub + Send + Sync> Creds for C {}
 /// A trait for types which store credentials.
 pub trait CredStore {
     /// The type of the credential handle returned by this store.
-    type CredHandle: Creds;
+    type CredHandle: 'static + Creds;
     /// The type of the exported credentials returned by this store.
-    type ExportedCreds: Serialize + DeserializeOwned;
+    type ExportedCreds: 'static + Serialize + DeserializeOwned;
 
     /// Returns the node credentials. If credentials haven't been generated, they are generated
     /// stored and returned.

+ 4 - 1
crates/btlib/src/crypto/file_cred_store.rs

@@ -10,7 +10,7 @@ use btserde::{read_from, write_to};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use std::{
     fmt::Display,
-    fs::{File, Metadata, OpenOptions},
+    fs::{DirBuilder, File, Metadata, OpenOptions},
     io::{self, BufReader, BufWriter},
     os::unix::fs::PermissionsExt,
     path::{Path, PathBuf},
@@ -135,6 +135,9 @@ mod private {
                 Ok(file) => file,
                 Err(err) => {
                     if io::ErrorKind::NotFound == err.kind() {
+                        if let Some(dir_path) = file_path.parent() {
+                            DirBuilder::new().recursive(true).create(dir_path)?;
+                        }
                         OpenOptions::new()
                             .read(true)
                             .write(true)

+ 3 - 2
crates/btmsg/src/lib.rs

@@ -9,7 +9,7 @@ use callback_framed::CallbackFramed;
 pub use callback_framed::DeserCallback;
 
 use btlib::{bterr, crypto::Creds, error::DisplayErr, BlockPath, Result, Writecap};
-use btserde::write_to;
+use btserde::{field_helpers::smart_ptr, write_to};
 use bytes::{BufMut, BytesMut};
 use core::{
     future::{ready, Future, Ready},
@@ -87,9 +87,10 @@ pub trait SendMsg<'de>: CallMsg<'de> {}
 
 /// An address which identifies a block on the network. An instance of this struct can be
 /// used to get a socket address for the block this address refers to.
-#[derive(PartialEq, Eq, Hash, Clone, Debug)]
+#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
 pub struct BlockAddr {
     ip_addr: IpAddr,
+    #[serde(with = "smart_ptr")]
     path: Arc<BlockPath>,
 }
 

+ 34 - 17
crates/btprovision/src/main.rs

@@ -15,7 +15,7 @@ use std::{
 };
 use termion::input::TermRead;
 
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
 struct AppConfig {
     /// The configuration for the [CredStore] to use.
     credstore: CredStoreCfg,
@@ -34,7 +34,9 @@ impl AppConfig {
         let expires = Epoch::now() + Duration::from_secs(DEFAULT_VALID_FOR);
         Ok(Config::builder()
             .set_default("writecapexpires", expires.value().to_string().as_str())?
-            .btconfig()
+            .set_default("writecappath", "")?
+            .set_default("writecapsavepath", "./state/node_writecap")?
+            .btconfig()?
             .build()?
             .try_deserialize()?)
     }
@@ -119,11 +121,6 @@ impl<'a> CredStoreConsumer for IssueWritecapConsumer<'a> {
 }
 
 fn issue_node_writecap(config: AppConfig) -> Result<()> {
-    let password = if let Some(password) = config.password {
-        password
-    } else {
-        password_prompt("Please enter the root password: ")?
-    };
     let (writecap_path, cred_path, issuee, expires) = get_settings!(
         config,
         writecapsavepath,
@@ -131,6 +128,11 @@ fn issue_node_writecap(config: AppConfig) -> Result<()> {
         writecapissuee,
         writecapexpires
     );
+    let password = if let Some(password) = config.password {
+        password
+    } else {
+        password_prompt("Please enter the root password: ")?
+    };
     let cred_components = RelBlockPath::try_from(cred_path.as_str())?;
     let issuee = Principal::try_from(issuee.as_str())?;
     let writecap = config.credstore.consume(IssueWritecapConsumer {
@@ -177,12 +179,31 @@ fn save_node_writecap(config: AppConfig) -> Result<()> {
         .consume(SaveNodeWritecapConsumer { writecap })?
 }
 
+struct PrincipalConsumer;
+
+impl CredStoreConsumer for PrincipalConsumer {
+    type Output = Result<Principal>;
+    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+        Ok(cred_store.node_creds()?.principal())
+    }
+}
+
+fn all(mut config: AppConfig) -> Result<()> {
+    gen_root_creds(config.clone())?;
+    gen_node_creds(config.clone())?;
+    let node_principal = config.credstore.clone().consume(PrincipalConsumer)??;
+    config.writecapissuee = Some(node_principal.to_string());
+    issue_node_writecap(config.clone())?;
+    save_node_writecap(config)
+}
+
 fn run(command: &str, config: AppConfig) -> Result<()> {
     match command {
         "gen_root_creds" => gen_root_creds(config),
         "gen_node_creds" => gen_node_creds(config),
         "issue_node_writecap" => issue_node_writecap(config),
         "save_node_writecap" => save_node_writecap(config),
+        "all" => all(config),
         _ => Err(bterr!("unrecognized command: {command}")),
     }
 }
@@ -203,15 +224,6 @@ mod tests {
     use btlib::{crypto::CredsPriv, BlockError};
     use tempdir::TempDir;
 
-    struct PrincipalConsumer;
-
-    impl CredStoreConsumer for PrincipalConsumer {
-        type Output = String;
-        fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
-            cred_store.node_creds().unwrap().principal().to_string()
-        }
-    }
-
     struct NodeWritecapConsumer;
 
     impl CredStoreConsumer for NodeWritecapConsumer {
@@ -293,7 +305,12 @@ mod tests {
             },
         )
         .unwrap();
-        let issued_to_expected = node_store.clone().consume(PrincipalConsumer).unwrap();
+        let issued_to_expected = node_store
+            .clone()
+            .consume(PrincipalConsumer)
+            .unwrap()
+            .unwrap()
+            .to_string();
         run(
             "issue_node_writecap",
             AppConfig {

+ 4 - 1
crates/btserde/Cargo.toml

@@ -8,4 +8,7 @@ edition = "2021"
 
 [dependencies]
 serde = { version = "^1.0.136", features = ["derive"] }
-serde-big-array = { version = "^0.4.1" }
+serde-big-array = { version = "^0.4.1" }
+
+[dev-dependencies]
+paste = "1.0.11"

+ 91 - 0
crates/btserde/src/field_helpers.rs

@@ -0,0 +1,91 @@
+//! Modules that help with serializing and deserializing struct fields via the
+//! `#[serde(with ="path")]` attribute.
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// A helper for serializing and deserializing fields which are "smart pointers", types like
+/// [Arc], [Rc], and [Box].
+pub mod smart_ptr {
+    use super::*;
+    use std::{ops::Deref, rc::Rc, sync::Arc};
+
+    /// Trait for general smart pointers.
+    pub trait SmartPtr: Deref {
+        /// Creates a new pointer which points to the given target.
+        fn new(inner: Self::Target) -> Self;
+    }
+
+    macro_rules! impl_smart_ptr {
+        ($ptr:ident) => {
+            impl<T> SmartPtr for $ptr<T> {
+                fn new(inner: Self::Target) -> Self {
+                    $ptr::new(inner)
+                }
+            }
+        };
+    }
+
+    impl_smart_ptr!(Arc);
+    impl_smart_ptr!(Box);
+    impl_smart_ptr!(Rc);
+
+    pub fn serialize<P, T, S>(field: &P, ser: S) -> Result<S::Ok, S::Error>
+    where
+        P: SmartPtr<Target = T>,
+        T: ?Sized + Serialize,
+        S: Serializer,
+    {
+        field.deref().serialize(ser)
+    }
+
+    pub fn deserialize<'de, P, T, D>(de: D) -> Result<P, D::Error>
+    where
+        P: SmartPtr<Target = T>,
+        T: ?Sized + Deserialize<'de>,
+        D: Deserializer<'de>,
+    {
+        Ok(P::new(T::deserialize(de)?))
+    }
+}
+
+#[cfg(test)]
+mod smart_ptr_tests {
+    use crate::{from_vec, to_vec};
+    use paste::paste;
+    use serde::{Deserialize, Serialize};
+    use std::{rc::Rc, sync::Arc};
+
+    use super::smart_ptr::{self, SmartPtr};
+
+    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
+    struct Test<P: SmartPtr<Target = String>> {
+        #[serde(with = "smart_ptr")]
+        field: P,
+    }
+
+    impl<P: SmartPtr<Target = String>> Test<P> {
+        fn new(expected: &str) -> Self {
+            Test {
+                field: P::new(expected.to_string()),
+            }
+        }
+    }
+
+    macro_rules! check_ptr {
+        ($ptr:ty) => {
+            paste! {
+                #[test]
+                fn [<"check_" $ptr:lower>]() {
+                    let expected = Test::new("expected");
+
+                    let actual: Test<$ptr<String>> = from_vec(&to_vec(&expected).unwrap()).unwrap();
+
+                    assert_eq!(expected, actual);
+                }
+            }
+        };
+    }
+
+    check_ptr!(Arc);
+    check_ptr!(Box);
+    check_ptr!(Rc);
+}

+ 1 - 0
crates/btserde/src/lib.rs

@@ -3,6 +3,7 @@
 
 mod de;
 mod error;
+pub mod field_helpers;
 mod reader;
 mod ser;
 

+ 14 - 9
dockerfiles/btfsd.Dockerfile → dockerfiles/Dockerfile

@@ -1,6 +1,8 @@
 FROM archlinux:base-devel AS build
+ARG BT_APP
+ARG BT_DEPENDS
 RUN pacman -Syu --noconfirm
-RUN pacman -S --noconfirm rustup clang openssl tpm2-tss
+RUN pacman -S --noconfirm rustup clang $BT_DEPENDS
 RUN rustup toolchain install nightly
 RUN rustup default nightly
 RUN mkdir -p src/crates
@@ -9,17 +11,20 @@ COPY Cargo.toml /src/
 COPY Cargo.lock /src/
 WORKDIR /src
 RUN cargo fetch --target x86_64-unknown-linux-gnu
-WORKDIR /src/crates/btfsd
+WORKDIR /src/crates/$BT_APP
 RUN cargo build --release
 
 FROM archlinux:base AS release
+ARG BT_APP
+ARG BT_DEPENDS
+ENV BT_APP=$BT_APP
 RUN pacman -Syu --noconfirm\
-    && pacman -S --noconfirm openssl tpm2-tss swtpm tpm2-abrmd dbus\
+    && pacman -S --noconfirm $BT_DEPENDS\
     && pacman -Scc --noconfirm\
-    && useradd btfsd\
+    && useradd $BT_APP \
     && mkdir -p /app/state\
-    && chown btfsd:btfsd /app/state
-COPY --from=build /src/target/release/btfsd /app/
-COPY --from=build /src/crates/btfsd/btfsd_init.sh /init.sh
-USER btfsd
-ENTRYPOINT ["/init.sh"]
+    && chown $BT_APP:$BT_APP /app/state
+COPY --from=build /src/target/release/$BT_APP /app/app.bin
+USER $BT_APP
+WORKDIR /app
+ENTRYPOINT ["./app.bin"]

+ 23 - 0
dockerfiles/build_image.sh

@@ -0,0 +1,23 @@
+#!/bin/sh
+set -e
+
+if [ -z "$BT_APP" ]; then
+    echo The enironment variable BT_APP must be set to the name of the crate to build.
+    exit 1
+fi
+
+BASE_DEPENDS='openssl tpm2-tss'
+if [ btfsd = "$BT_APP" ]; then
+    BT_DEPENDS="$BASE_DEPENDS"
+elif [ btprovision = "$BT_APP" ]; then
+    BT_DEPENDS="$BASE_DEPENDS"
+elif [ btfuse = "$BT_APP" ]; then
+    BT_DEPENDS="$BASE_DEPENDS fuse3"
+else 
+    echo Unrecognized BT_APP: $BT_APP
+    exit 2
+fi
+
+sudo docker build -t $BT_APP:latest\
+  --build-arg BT_APP=$BT_APP --build-arg BT_DEPENDS="$BT_DEPENDS"\
+  -f Dockerfile ..

+ 0 - 4
dockerfiles/build_images.sh

@@ -1,4 +0,0 @@
-#!/bin/sh
-set -e
-
-sudo docker build -t btfsd:latest -f btfsd.Dockerfile ..

+ 0 - 5
dockerfiles/run_btfsd.sh

@@ -1,5 +0,0 @@
-#!/bin/sh
-sudo docker run -d --rm --name btfsd\
-    --env BTFSD_USESWTPM=true\
-    --env RUST_LOG='warn,btfsd=debug'\
-    btfsd:latest password

+ 12 - 0
dockerfiles/run_container.sh

@@ -0,0 +1,12 @@
+#!/bin/sh
+
+if [ -z "$BT_APP" ]; then
+    echo The enironment variable BT_APP must be set to the name of the crate to build.
+    exit 1
+fi
+
+mkdir -p ~/.config/blocktree/$BT_APP
+sudo docker run --rm --name $BT_APP\
+    -v ~/.config/blocktree:/app/state\
+    --env RUST_LOG=warn\
+    $BT_APP:latest "$@"