diff --git a/src/engine/strat_engine/backstore/crypt/handle.rs b/src/engine/strat_engine/backstore/crypt/handle.rs index f5bcbc84da7..58f9b570746 100644 --- a/src/engine/strat_engine/backstore/crypt/handle.rs +++ b/src/engine/strat_engine/backstore/crypt/handle.rs @@ -11,7 +11,7 @@ use std::{ use either::Either; use serde_json::Value; -use devicemapper::{Device, Sectors}; +use devicemapper::{Device, DmName, DmNameBuf, Sectors}; use libcryptsetup_rs::{ c_uint, consts::{flags::CryptActivate, vals::EncryptionFormat}, @@ -64,7 +64,7 @@ impl CryptHandle { physical_path: DevicePath, identifiers: StratisIdentifiers, encryption_info: EncryptionInfo, - activation_name: String, + activation_name: DmNameBuf, pool_name: Option, ) -> StratisResult { let device = get_devno_from_path(&physical_path)?; @@ -82,8 +82,8 @@ impl CryptHandle { metadata_handle: CryptMetadataHandle, ) -> StratisResult { let activated_path = DevicePath::new( - &once(DEVICEMAPPER_PATH) - .chain(once(metadata_handle.activation_name())) + &once(DEVICEMAPPER_PATH.to_string()) + .chain(once(metadata_handle.activation_name().to_string())) .collect::(), )?; Ok(CryptHandle { @@ -138,7 +138,7 @@ impl CryptHandle { } /// Return the name of the activated devicemapper device. - pub fn activation_name(&self) -> &str { + pub fn activation_name(&self) -> &DmName { self.metadata_handle.activation_name() } @@ -408,7 +408,7 @@ impl CryptHandle { let name = self.activation_name().to_owned(); let active_device = log_on_failure!( self.acquire_crypt_device()? - .runtime_handle(&name) + .runtime_handle(&name.to_string()) .get_active_device(), "Failed to get device size for encrypted logical device" ); @@ -449,7 +449,7 @@ impl CryptHandle { )?; crypt .context_handle() - .resize(self.activation_name(), processed_size) + .resize(&self.activation_name().to_string(), processed_size) .map_err(StratisError::Crypt) } } diff --git a/src/engine/strat_engine/backstore/crypt/initialize.rs b/src/engine/strat_engine/backstore/crypt/initialize.rs index 7a4a84257e8..c17a6816612 100644 --- a/src/engine/strat_engine/backstore/crypt/initialize.rs +++ b/src/engine/strat_engine/backstore/crypt/initialize.rs @@ -5,8 +5,9 @@ use std::path::Path; use either::Either; -use serde_json::Value; +use serde_json::{to_value, Value}; +use devicemapper::{DmName, DmNameBuf}; use libcryptsetup_rs::{ consts::{ flags::CryptVolumeKey, @@ -43,7 +44,7 @@ use crate::{ pub struct CryptInitializer { physical_path: DevicePath, identifiers: StratisIdentifiers, - activation_name: String, + activation_name: DmNameBuf, } impl CryptInitializer { @@ -52,7 +53,7 @@ impl CryptInitializer { pool_uuid: PoolUuid, dev_uuid: DevUuid, ) -> CryptInitializer { - let dm_name = format_crypt_name(&dev_uuid).to_string(); + let dm_name = format_crypt_name(&dev_uuid); CryptInitializer { physical_path, activation_name: dm_name, @@ -237,12 +238,11 @@ impl CryptInitializer { log_on_failure!( device.token_handle().json_set(TokenInput::ReplaceToken( STRATIS_TOKEN_ID, - &StratisLuks2Token { + &to_value(&StratisLuks2Token { devname: self.activation_name.clone(), identifiers: self.identifiers, pool_name: Some(pool_name.clone()), - } - .into(), + })?, )), "Failed to create the Stratis token" ); @@ -260,7 +260,7 @@ impl CryptInitializer { pub fn rollback( device: &mut CryptDevice, physical_path: &Path, - name: &str, + name: &DmName, ) -> StratisResult<()> { ensure_wiped(device, physical_path, name) } diff --git a/src/engine/strat_engine/backstore/crypt/macros.rs b/src/engine/strat_engine/backstore/crypt/macros.rs index 68d583ad5e2..465149ef42b 100644 --- a/src/engine/strat_engine/backstore/crypt/macros.rs +++ b/src/engine/strat_engine/backstore/crypt/macros.rs @@ -15,44 +15,3 @@ macro_rules! log_on_failure { result? }} } - -macro_rules! check_key { - ($condition:expr, $key:tt, $value:tt) => { - if $condition { - return Err($crate::stratis::StratisError::Msg(format!( - "Stratis token key '{}' requires a value of '{}'", - $key, $value, - ))); - } - }; -} - -macro_rules! check_and_get_key { - ($get:expr, $key:tt) => { - if let Some(v) = $get { - v - } else { - return Err($crate::stratis::StratisError::Msg(format!( - "Stratis token is missing key '{}' or the value is of the wrong type", - $key - ))); - } - }; - ($get:expr, $func:expr, $key:tt, $ty:ty) => { - if let Some(ref v) = $get { - $func(v).map_err(|e| { - $crate::stratis::StratisError::Msg(format!( - "Failed to convert value for key '{}' to type {}: {}", - $key, - stringify!($ty), - e - )) - })? - } else { - return Err($crate::stratis::StratisError::Msg(format!( - "Stratis token is missing key '{}' or the value is of the wrong type", - $key - ))); - } - }; -} diff --git a/src/engine/strat_engine/backstore/crypt/metadata_handle.rs b/src/engine/strat_engine/backstore/crypt/metadata_handle.rs index 9fdc1851f6e..9da6d6abc46 100644 --- a/src/engine/strat_engine/backstore/crypt/metadata_handle.rs +++ b/src/engine/strat_engine/backstore/crypt/metadata_handle.rs @@ -4,7 +4,7 @@ use std::path::Path; -use devicemapper::Device; +use devicemapper::{Device, DmName, DmNameBuf}; use crate::{ engine::{ @@ -23,7 +23,7 @@ pub struct CryptMetadataHandle { pub(super) physical_path: DevicePath, pub(super) identifiers: StratisIdentifiers, pub(super) encryption_info: EncryptionInfo, - pub(super) activation_name: String, + pub(super) activation_name: DmNameBuf, pub(super) pool_name: Option, pub(super) device: Device, } @@ -33,7 +33,7 @@ impl CryptMetadataHandle { physical_path: DevicePath, identifiers: StratisIdentifiers, encryption_info: EncryptionInfo, - activation_name: String, + activation_name: DmNameBuf, pool_name: Option, device: Device, ) -> Self { @@ -72,7 +72,7 @@ impl CryptMetadataHandle { } /// Get the name of the activated device when it is activated. - pub fn activation_name(&self) -> &str { + pub fn activation_name(&self) -> &DmName { &self.activation_name } diff --git a/src/engine/strat_engine/backstore/crypt/mod.rs b/src/engine/strat_engine/backstore/crypt/mod.rs index 89bf86b8d31..29c2f169b08 100644 --- a/src/engine/strat_engine/backstore/crypt/mod.rs +++ b/src/engine/strat_engine/backstore/crypt/mod.rs @@ -274,11 +274,11 @@ mod tests { libc::close(fd); }; - let device_name = handle.activation_name().to_owned(); + let device_name = handle.activation_name(); loop { match libcryptsetup_rs::status( Some(&mut handle.acquire_crypt_device().unwrap()), - &device_name, + &device_name.to_string(), ) { Ok(CryptStatusInfo::Busy) => (), Ok(CryptStatusInfo::Active) => break, diff --git a/src/engine/strat_engine/backstore/crypt/shared.rs b/src/engine/strat_engine/backstore/crypt/shared.rs index 9810f99696f..0c8bbb68994 100644 --- a/src/engine/strat_engine/backstore/crypt/shared.rs +++ b/src/engine/strat_engine/backstore/crypt/shared.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::{ + fmt::{self, Formatter}, fs::OpenOptions, io::Write, path::{Path, PathBuf}, @@ -10,12 +11,17 @@ use std::{ use data_encoding::BASE64URL_NOPAD; use either::Either; -use retry::{delay::Fixed, retry_with_index, Error}; -use serde_json::{Map, Value}; +use retry::{delay::Fixed, retry_with_index, Error as RetryError}; +use serde::{ + de::{Error, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_json::{from_value, to_value, Map, Value}; use sha2::{Digest, Sha256}; use tempfile::TempDir; -use devicemapper::Bytes; +use devicemapper::{Bytes, DmName, DmNameBuf}; use libcryptsetup_rs::{ c_uint, consts::{ @@ -24,7 +30,7 @@ use libcryptsetup_rs::{ CryptDebugLevel, CryptLogLevel, CryptStatusInfo, CryptWipePattern, EncryptionFormat, }, }, - set_debug_level, set_log_callback, CryptDevice, CryptInit, LibcryptErr, TokenInput, + set_debug_level, set_log_callback, CryptDevice, CryptInit, TokenInput, }; use crate::{ @@ -75,83 +81,178 @@ pub fn set_up_crypt_logging() { } pub struct StratisLuks2Token { - pub devname: String, + pub devname: DmNameBuf, pub identifiers: StratisIdentifiers, pub pool_name: Option, } -impl Into for StratisLuks2Token { - fn into(self) -> Value { - let mut object = json!({ - TOKEN_TYPE_KEY: STRATIS_TOKEN_TYPE, - TOKEN_KEYSLOTS_KEY: [], - STRATIS_TOKEN_DEVNAME_KEY: self.devname, - STRATIS_TOKEN_POOL_UUID_KEY: self.identifiers.pool_uuid.to_string(), - STRATIS_TOKEN_DEV_UUID_KEY: self.identifiers.device_uuid.to_string(), - }); - if let Some(o) = object.as_object_mut() { - if let Some(n) = self.pool_name { - o.insert( - STRATIS_TOKEN_POOLNAME_KEY.to_string(), - Value::from(n.to_string()), - ); - } - } - object +impl Serialize for StratisLuks2Token { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_serializer = serializer.serialize_map(None)?; + map_serializer.serialize_entry(TOKEN_TYPE_KEY, STRATIS_TOKEN_TYPE)?; + map_serializer.serialize_entry::<_, [u32; 0]>(TOKEN_KEYSLOTS_KEY, &[])?; + map_serializer.serialize_entry(STRATIS_TOKEN_DEVNAME_KEY, &self.devname.to_string())?; + map_serializer.serialize_entry( + STRATIS_TOKEN_POOL_UUID_KEY, + &self.identifiers.pool_uuid.to_string(), + )?; + map_serializer.serialize_entry( + STRATIS_TOKEN_DEV_UUID_KEY, + &self.identifiers.device_uuid.to_string(), + )?; + map_serializer.end() } } -impl<'a> TryFrom<&'a Value> for StratisLuks2Token { - type Error = StratisError; +impl<'de> Deserialize<'de> for StratisLuks2Token { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StratisTokenVisitor; - fn try_from(v: &Value) -> StratisResult { - let map = if let Value::Object(m) = v { - m - } else { - return Err(StratisError::Crypt(LibcryptErr::InvalidConversion)); - }; + impl<'de> Visitor<'de> for StratisTokenVisitor { + type Value = StratisLuks2Token; - check_key!( - map.get(TOKEN_TYPE_KEY).and_then(|v| v.as_str()) != Some(STRATIS_TOKEN_TYPE), - "type", - STRATIS_TOKEN_TYPE - ); - check_key!( - map.get(TOKEN_KEYSLOTS_KEY).and_then(|v| v.as_array()) != Some(&Vec::new()), - "keyslots", - "[]" - ); - let devname = check_and_get_key!( - map.get(STRATIS_TOKEN_DEVNAME_KEY) - .and_then(|s| s.as_str()) - .map(|s| s.to_string()), - STRATIS_TOKEN_DEVNAME_KEY - ); - let pool_uuid = check_and_get_key!( - map.get(STRATIS_TOKEN_POOL_UUID_KEY) - .and_then(|s| s.as_str()) - .map(|s| s.to_string()), - PoolUuid::parse_str, - STRATIS_TOKEN_POOL_UUID_KEY, - PoolUuid - ); - let dev_uuid = check_and_get_key!( - map.get(STRATIS_TOKEN_DEV_UUID_KEY) - .and_then(|s| s.as_str()) - .map(|s| s.to_string()), - DevUuid::parse_str, - STRATIS_TOKEN_DEV_UUID_KEY, - DevUuid - ); - let pool_name = map - .get(STRATIS_TOKEN_POOLNAME_KEY) - .and_then(|s| s.as_str()) - .map(|s| Name::new(s.to_string())); - Ok(StratisLuks2Token { - devname, - identifiers: StratisIdentifiers::new(pool_uuid, dev_uuid), - pool_name, - }) + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "a Stratis LUKS2 token") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut token_type = None; + let mut token_keyslots = None; + let mut d_name = None; + let mut p_uuid = None; + let mut d_uuid = None; + let mut p_name = None; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + TOKEN_TYPE_KEY => { + token_type = Some(v); + } + TOKEN_KEYSLOTS_KEY => { + token_keyslots = Some(v); + } + STRATIS_TOKEN_DEVNAME_KEY => { + d_name = Some(v); + } + STRATIS_TOKEN_POOL_UUID_KEY => { + p_uuid = Some(v); + } + STRATIS_TOKEN_DEV_UUID_KEY => { + d_uuid = Some(v); + } + STRATIS_TOKEN_POOLNAME_KEY => { + p_name = Some(v); + } + st => { + return Err(A::Error::custom(format!("Found unrecognized key {st}"))); + } + } + } + + token_type + .ok_or_else(|| A::Error::custom(format!("Missing field {TOKEN_TYPE_KEY}"))) + .and_then(|ty| match ty { + Value::String(s) => { + if s == STRATIS_TOKEN_TYPE { + Ok(()) + } else { + Err(A::Error::custom(format!( + "Incorrect value for {TOKEN_TYPE_KEY}: {s}" + ))) + } + } + _ => Err(A::Error::custom(format!( + "Unrecognized value type for {TOKEN_TYPE_KEY}" + ))), + }) + .and_then(|_| { + let value = token_keyslots.ok_or_else(|| { + A::Error::custom(format!("Missing field {TOKEN_KEYSLOTS_KEY}")) + })?; + match value { + Value::Array(a) => { + if a.is_empty() { + Ok(()) + } else { + Err(A::Error::custom(format!( + "Found non-empty array for {TOKEN_KEYSLOTS_KEY}" + ))) + } + } + _ => Err(A::Error::custom(format!( + "Unrecognized value type for {TOKEN_TYPE_KEY}" + ))), + } + }) + .and_then(|_| { + let value = d_name.ok_or_else(|| { + A::Error::custom(format!("Missing field {STRATIS_TOKEN_DEVNAME_KEY}")) + })?; + match value { + Value::String(s) => DmNameBuf::new(s).map_err(A::Error::custom), + _ => Err(A::Error::custom(format!( + "Unrecognized value type for {STRATIS_TOKEN_DEVNAME_KEY}" + ))), + } + }) + .and_then(|dev_name| { + let value = p_uuid.ok_or_else(|| { + A::Error::custom(format!("Missing field {STRATIS_TOKEN_POOL_UUID_KEY}")) + })?; + match value { + Value::String(s) => PoolUuid::parse_str(&s) + .map(|uuid| (dev_name, uuid)) + .map_err(A::Error::custom), + _ => Err(A::Error::custom(format!( + "Unrecognized value type for {STRATIS_TOKEN_POOL_UUID_KEY}" + ))), + } + }) + .and_then(|(dev_name, pool_uuid)| { + let value = d_uuid.ok_or_else(|| { + A::Error::custom(format!("Missing field {STRATIS_TOKEN_DEV_UUID_KEY}")) + })?; + match value { + Value::String(s) => DevUuid::parse_str(&s) + .map(|uuid| (dev_name, pool_uuid, uuid)) + .map_err(A::Error::custom), + _ => Err(A::Error::custom(format!( + "Unrecognized value type for {STRATIS_TOKEN_DEV_UUID_KEY}" + ))), + } + }) + .and_then(|(devname, pool_uuid, device_uuid)| { + let pool_name = match p_name { + Some(Value::String(s)) => Some(Name::new(s)), + Some(_) => { + return Err(A::Error::custom(format!( + "Unrecognized value type for {STRATIS_TOKEN_POOLNAME_KEY}" + ))) + } + None => None, + }; + Ok(StratisLuks2Token { + devname, + identifiers: StratisIdentifiers { + pool_uuid, + device_uuid, + }, + pool_name, + }) + }) + } + } + + deserializer.deserialize_map(StratisTokenVisitor) } } @@ -339,7 +440,7 @@ pub fn setup_crypt_handle( } Some(UnlockMethod::Clevis) => activate(Either::Right(physical_path), &name)?, None => { - if let Err(_) | Ok(CryptStatusInfo::Inactive | CryptStatusInfo::Invalid) = libcryptsetup_rs::status(Some(device), &name) { + if let Err(_) | Ok(CryptStatusInfo::Inactive | CryptStatusInfo::Invalid) = libcryptsetup_rs::status(Some(device), &name.to_string()) { return Err(StratisError::Msg( "Found a crypt device but it is not activated and no unlock method was provided".to_string(), )); @@ -608,8 +709,8 @@ fn is_encrypted_stratis_device(device: &mut CryptDevice) -> bool { .unwrap_or(false) } -fn device_is_active(device: Option<&mut CryptDevice>, device_name: &str) -> StratisResult<()> { - match libcryptsetup_rs::status(device, device_name) { +fn device_is_active(device: Option<&mut CryptDevice>, device_name: &DmName) -> StratisResult<()> { + match libcryptsetup_rs::status(device, &device_name.to_string()) { Ok(CryptStatusInfo::Active) => Ok(()), Ok(CryptStatusInfo::Busy) => { info!( @@ -649,11 +750,11 @@ fn device_is_active(device: Option<&mut CryptDevice>, device_name: &str) -> Stra /// /// Precondition: The key description has been verified to be present in the keyring /// if matches!(unlock_method, UnlockMethod::Keyring). -fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &str) -> StratisResult<()> { +fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &DmName) -> StratisResult<()> { // Activate by token log_on_failure!( crypt_device.token_handle().activate_by_token::<()>( - Some(name), + Some(&name.to_string()), Some(LUKS2_TOKEN_ID), None, CryptActivate::empty(), @@ -668,7 +769,7 @@ fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &str) -> StratisR /// Stratis token. pub fn activate( unlock_param: Either<(&mut CryptDevice, &KeyDescription), &Path>, - name: &str, + name: &DmName, ) -> StratisResult<()> { let crypt_device = match unlock_param { Either::Left((device, kd)) => { @@ -752,9 +853,9 @@ pub fn get_keyslot_number( /// a destructive action. `name` should be the name of the device as registered /// with devicemapper and cryptsetup. This method is idempotent and leaves /// the state as inactive. -pub fn ensure_inactive(device: &mut CryptDevice, name: &str) -> StratisResult<()> { +pub fn ensure_inactive(device: &mut CryptDevice, name: &DmName) -> StratisResult<()> { let status = log_on_failure!( - libcryptsetup_rs::status(Some(device), name), + libcryptsetup_rs::status(Some(device), &name.to_string()), "Failed to determine status of device with name {}", name ); @@ -763,7 +864,7 @@ pub fn ensure_inactive(device: &mut CryptDevice, name: &str) -> StratisResult<() log_on_failure!( device .activate_handle() - .deactivate(name, CryptDeactivate::empty()), + .deactivate(&name.to_string(), CryptDeactivate::empty()), "Failed to deactivate the crypt device with name {}", name ); @@ -773,16 +874,16 @@ pub fn ensure_inactive(device: &mut CryptDevice, name: &str) -> StratisResult<() trace!("Crypt device deactivate attempt {}", i); device .activate_handle() - .deactivate(name, CryptDeactivate::empty()) + .deactivate(&name.to_string(), CryptDeactivate::empty()) .map_err(StratisError::Crypt) }) .map_err(|e| match e { - Error::Internal(s) => StratisError::Chained( + RetryError::Internal(s) => StratisError::Chained( "Retries for crypt device deactivation failed with an internal error" .to_string(), Box::new(StratisError::Msg(s)), ), - Error::Operation { error, .. } => error, + RetryError::Operation { error, .. } => error, })?; } _ => (), @@ -836,9 +937,8 @@ pub fn wipe_fallback(path: &Path, causal_error: StratisError) -> StratisError { pub fn ensure_wiped( device: &mut CryptDevice, physical_path: &Path, - name: &str, + name: &DmName, ) -> StratisResult<()> { - ensure_inactive(device, name)?; let keyslot_number = get_keyslot_number(device, LUKS2_TOKEN_ID); match keyslot_number { Ok(Some(nums)) => { @@ -931,7 +1031,7 @@ fn luks2_token_type_is_valid(json: &Value) -> bool { fn stratis_token_is_valid(json: &Value) -> bool { debug!("Stratis LUKS2 token: {}", json); - let result = StratisLuks2Token::try_from(json); + let result = from_value::(json.clone()); if let Err(ref e) = result { debug!( "LUKS2 token in the Stratis token slot does not appear \ @@ -963,7 +1063,7 @@ pub fn read_key(key_description: &KeyDescription) -> StratisResult StratisResult { +fn activation_name_from_metadata(device: &mut CryptDevice) -> StratisResult { let json = log_on_failure!( device.token_handle().json_get(STRATIS_TOKEN_ID), "Failed to get Stratis JSON token from LUKS2 metadata" @@ -986,7 +1086,8 @@ fn activation_name_from_metadata(device: &mut CryptDevice) -> StratisResult Option { /// Query the Stratis metadata for the pool name. pub fn pool_name_from_metadata(device: &mut CryptDevice) -> StratisResult> { - Ok(StratisLuks2Token::try_from(&device.token_handle().json_get(STRATIS_TOKEN_ID)?)?.pool_name) + Ok( + from_value::(device.token_handle().json_get(STRATIS_TOKEN_ID)?)? + .pool_name, + ) } /// Replace the old pool name in the Stratis LUKS2 token. pub fn replace_pool_name(device: &mut CryptDevice, new_name: Name) -> StratisResult<()> { let mut token = - StratisLuks2Token::try_from(&device.token_handle().json_get(STRATIS_TOKEN_ID)?)?; + from_value::(device.token_handle().json_get(STRATIS_TOKEN_ID)?)?; token.pool_name = Some(new_name); - device - .token_handle() - .json_set(TokenInput::ReplaceToken(STRATIS_TOKEN_ID, &token.into()))?; + device.token_handle().json_set(TokenInput::ReplaceToken( + STRATIS_TOKEN_ID, + &to_value(token)?, + ))?; Ok(()) } diff --git a/src/engine/strat_engine/cmd.rs b/src/engine/strat_engine/cmd.rs index 689ccbf6f29..4877ed81214 100644 --- a/src/engine/strat_engine/cmd.rs +++ b/src/engine/strat_engine/cmd.rs @@ -26,7 +26,7 @@ use libc::c_uint; use libcryptsetup_rs::SafeMemHandle; use serde_json::Value; -use devicemapper::{MetaBlocks, Sectors}; +use devicemapper::{DmName, MetaBlocks, Sectors}; use crate::{ engine::{ @@ -339,7 +339,7 @@ pub fn clevis_luks_unbind(dev_path: &Path, keyslot: libc::c_uint) -> StratisResu } /// Unlock a device using the clevis CLI. -pub fn clevis_luks_unlock(dev_path: &Path, dm_name: &str) -> StratisResult<()> { +pub fn clevis_luks_unlock(dev_path: &Path, dm_name: &DmName) -> StratisResult<()> { execute_cmd( Command::new(get_clevis_executable(CLEVIS)?) .arg("luks") @@ -347,7 +347,7 @@ pub fn clevis_luks_unlock(dev_path: &Path, dm_name: &str) -> StratisResult<()> { .arg("-d") .arg(dev_path.display().to_string()) .arg("-n") - .arg(dm_name), + .arg(dm_name.to_string()), ) }