|
| 1 | +use crate::builtin::HAMT_BIT_WIDTH; |
| 2 | +use crate::{ActorError, AsActorError, Hasher}; |
| 3 | +use anyhow::anyhow; |
| 4 | +use cid::Cid; |
| 5 | +use fvm_ipld_blockstore::Blockstore; |
| 6 | +use fvm_ipld_hamt as hamt; |
| 7 | +use fvm_shared::error::ExitCode; |
| 8 | +use serde::de::DeserializeOwned; |
| 9 | +use serde::Serialize; |
| 10 | + |
| 11 | +/// Wraps a HAMT to provide a convenient map API. |
| 12 | +/// The key type is Vec<u8>, so conversion to/from interpretations must by done by the caller. |
| 13 | +/// Any errors are returned with exit code indicating illegal state. |
| 14 | +/// The name is not persisted in state, but adorns any error messages. |
| 15 | +pub struct Map2<'bs, BS, V> |
| 16 | +where |
| 17 | + BS: Blockstore, |
| 18 | + V: DeserializeOwned + Serialize, |
| 19 | +{ |
| 20 | + hamt: hamt::Hamt<&'bs BS, V, hamt::BytesKey, Hasher>, |
| 21 | + name: &'static str, |
| 22 | +} |
| 23 | + |
| 24 | +trait MapKey: Sized { |
| 25 | + fn from_bytes(b: &[u8]) -> Result<Self, String>; |
| 26 | + fn to_bytes(&self) -> Result<Vec<u8>, String>; |
| 27 | +} |
| 28 | + |
| 29 | +pub type Config = hamt::Config; |
| 30 | + |
| 31 | +pub const DEFAULT_CONF: Config = |
| 32 | + Config { bit_width: HAMT_BIT_WIDTH, min_data_depth: 0, max_array_width: 3 }; |
| 33 | + |
| 34 | +impl<'bs, BS, V> Map2<'bs, BS, V> |
| 35 | +where |
| 36 | + BS: Blockstore, |
| 37 | + V: DeserializeOwned + Serialize, |
| 38 | +{ |
| 39 | + /// Creates a new, empty map. |
| 40 | + pub fn empty(store: &'bs BS, config: Config, name: &'static str) -> Self { |
| 41 | + Self { hamt: hamt::Hamt::new_with_config(store, config), name } |
| 42 | + } |
| 43 | + |
| 44 | + /// Creates a new empty map and flushes it to the store. |
| 45 | + /// Returns the CID of the empty map root. |
| 46 | + pub fn flush_empty(store: &'bs BS, config: Config) -> Result<Cid, ActorError> { |
| 47 | + // This CID is constant regardless of the HAMT's configuration, so as an optimisation |
| 48 | + // we could hard-code it and merely check it is already stored. |
| 49 | + Self::empty(store, config, "empty").flush() |
| 50 | + } |
| 51 | + |
| 52 | + /// Loads a map from the store. |
| 53 | + // There is no version of this method that doesn't take an explicit config parameter. |
| 54 | + // The caller must know the configuration to interpret the HAMT correctly. |
| 55 | + // Forcing them to provide it makes it harder to accidentally use an incorrect default. |
| 56 | + pub fn load( |
| 57 | + store: &'bs BS, |
| 58 | + root: &Cid, |
| 59 | + config: Config, |
| 60 | + name: &'static str, |
| 61 | + ) -> Result<Self, ActorError> { |
| 62 | + Ok(Self { |
| 63 | + hamt: hamt::Hamt::load_with_config(root, store, config) |
| 64 | + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { |
| 65 | + format!("failed to load HAMT '{}'", name) |
| 66 | + })?, |
| 67 | + name, |
| 68 | + }) |
| 69 | + } |
| 70 | + |
| 71 | + /// Flushes the map's contents to the store. |
| 72 | + /// Returns the root node CID. |
| 73 | + pub fn flush(&mut self) -> Result<Cid, ActorError> { |
| 74 | + self.hamt.flush().with_context_code(ExitCode::USR_ILLEGAL_STATE, || { |
| 75 | + format!("failed to flush HAMT '{}'", self.name) |
| 76 | + }) |
| 77 | + } |
| 78 | + |
| 79 | + /// Returns a reference to the value associated with a key, if present. |
| 80 | + pub fn get(&self, key: &[u8]) -> Result<Option<&V>, ActorError> { |
| 81 | + self.hamt.get(key).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { |
| 82 | + format!("failed to get from HAMT '{}'", self.name) |
| 83 | + }) |
| 84 | + } |
| 85 | + |
| 86 | + /// Inserts a key-value pair into the map. |
| 87 | + /// Returns any value previously associated with the key. |
| 88 | + pub fn set(&mut self, key: &[u8], value: V) -> Result<Option<V>, ActorError> |
| 89 | + where |
| 90 | + V: PartialEq, |
| 91 | + { |
| 92 | + self.hamt.set(key.into(), value).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { |
| 93 | + format!("failed to set in HAMT '{}'", self.name) |
| 94 | + }) |
| 95 | + } |
| 96 | + |
| 97 | + /// Inserts a key-value pair only if the key does not already exist. |
| 98 | + /// Returns whether the map was modified (i.e. key was absent). |
| 99 | + pub fn set_if_absent(&mut self, key: &[u8], value: V) -> Result<bool, ActorError> |
| 100 | + where |
| 101 | + V: PartialEq, |
| 102 | + { |
| 103 | + self.hamt |
| 104 | + .set_if_absent(key.into(), value) |
| 105 | + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { |
| 106 | + format!("failed to set in HAMT '{}'", self.name) |
| 107 | + }) |
| 108 | + } |
| 109 | + |
| 110 | + /// Iterates over all key-value pairs in the map. |
| 111 | + pub fn for_each<F>(&self, mut f: F) -> Result<(), ActorError> |
| 112 | + where |
| 113 | + // Note the result type of F uses ActorError. |
| 114 | + // The implementation will extract and propagate any ActorError |
| 115 | + // wrapped in a hamt::Error::Dynamic. |
| 116 | + F: FnMut(&[u8], &V) -> Result<(), ActorError>, |
| 117 | + { |
| 118 | + match self.hamt.for_each(|k, v| f(k, v).map_err(|e| anyhow!(e))) { |
| 119 | + Ok(_) => Ok(()), |
| 120 | + Err(hamt_err) => match hamt_err { |
| 121 | + hamt::Error::Dynamic(e) => match e.downcast::<ActorError>() { |
| 122 | + Ok(ae) => Err(ae), |
| 123 | + Err(e) => Err(ActorError::illegal_state(format!( |
| 124 | + "error traversing HAMT {}: {}", |
| 125 | + self.name, e |
| 126 | + ))), |
| 127 | + }, |
| 128 | + e => Err(ActorError::illegal_state(format!( |
| 129 | + "error traversing HAMT {}: {}", |
| 130 | + self.name, e |
| 131 | + ))), |
| 132 | + }, |
| 133 | + } |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +#[cfg(test)] |
| 138 | +mod tests { |
| 139 | + use super::*; |
| 140 | + use fvm_ipld_blockstore::MemoryBlockstore; |
| 141 | + |
| 142 | + #[test] |
| 143 | + fn basic_put_get() { |
| 144 | + let bs = MemoryBlockstore::new(); |
| 145 | + let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty"); |
| 146 | + m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap(); |
| 147 | + assert!(m.get(&[1, 2]).unwrap().is_none()); |
| 148 | + assert_eq!(&"1234".to_string(), m.get(&[1, 2, 3, 4]).unwrap().unwrap()); |
| 149 | + } |
| 150 | + |
| 151 | + #[test] |
| 152 | + fn for_each_callback_exitcode_propagates() { |
| 153 | + let bs = MemoryBlockstore::new(); |
| 154 | + let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty"); |
| 155 | + m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap(); |
| 156 | + let res = m.for_each(|_, _| Err(ActorError::forbidden("test".to_string()))); |
| 157 | + assert!(res.is_err()); |
| 158 | + assert_eq!(res.unwrap_err(), ActorError::forbidden("test".to_string())); |
| 159 | + } |
| 160 | +} |
0 commit comments