//! An object mapper for looking up `rpc::Object`s by ID. //! //! This mapper stores strong or weak references, and uses a generational index //! to keep track of names for them. //! //! TODO RPC: Add an object diagram here once the implementation settles down. use std::sync::{Arc, Weak}; use rand::RngExt; use slotmap_careful::{Key as _, KeyData, SlotMap}; use tor_rpcbase as rpc; pub(crate) mod methods; slotmap_careful::new_key_type! { pub(crate) struct GenIdx; } /// A weak or a strong reference to an RPC object. // // Note: This type does not pack very efficiently, due to Rust's current lack // of alignment-based niche optimization. // If this ever matters, we can either use two slotmaps, or we can implement // some kind of kludgey hack on our own. #[derive(Clone, derive_more::From)] enum ObjectRef { /// A strong reference. Strong(Arc), /// A weak reference reference. Weak(Weak), } impl ObjectRef { /// Return this reference as an Arc, if it is present. fn get(&self) -> Option> { match self { ObjectRef::Strong(s) => Some(Arc::clone(s)), ObjectRef::Weak(w) => w.upgrade(), } } } /// A mechanism to look up RPC `Objects` by their `ObjectId`. #[derive(Default)] pub(crate) struct ObjMap { /// Generationally indexed arena of strong object references. arena: SlotMap, } /// Encoding functions for GenIdx. /// /// The encoding is deliberately nondeterministic: we want to avoid situations /// where applications depend on the details of our ObjectIds, or hardcode the /// ObjectIds they expect, or rely on the same weak generational index getting /// encoded the same way every time they see it. /// /// The encoding is deliberately non-cryptographic: we do not want to imply /// that this gives any security. It is just a mild deterrent to misuse. /// /// If you find yourself wanting to reverse-engineer this code so that you can /// analyze these object IDs, please contact the Arti developers instead and let /// us give you a better way to do whatever you want. impl GenIdx { /// The length of a byte-encoded (but not base-64 encoded) GenIdx. pub(crate) const BYTE_LEN: usize = 16; /// Encode `self` into an rpc::ObjectId that we can give to a client. pub(crate) fn encode(self) -> rpc::ObjectId { self.encode_with_rng(&mut rand::rng()) } /// As `encode`, but take a Rng as an argument. For testing. fn encode_with_rng(self, rng: &mut R) -> rpc::ObjectId { use base64ct::Encoding; let bytes = self.to_bytes(rng); rpc::ObjectId::from(base64ct::Base64UrlUnpadded::encode_string(&bytes[..])) } /// As `encode_with_rng`, but return an array of bytes. pub(crate) fn to_bytes(self, rng: &mut R) -> [u8; Self::BYTE_LEN] { use tor_bytes::Writer; let ffi_idx = self.data().as_ffi(); let x = rng.random::(); let mut bytes = Vec::with_capacity(Self::BYTE_LEN); bytes.write_u64(x); bytes.write_u64(ffi_idx.wrapping_add(x)); bytes.try_into().expect("Length was wrong!") } /// Attempt to decode `id` into a `GenIdx` than an ObjMap can use. pub(crate) fn try_decode(id: &rpc::ObjectId) -> Result { use base64ct::Encoding; let bytes = base64ct::Base64UrlUnpadded::decode_vec(id.as_ref()) .map_err(|_| rpc::LookupError::NoObject(id.clone()))?; Self::from_bytes(&bytes).ok_or_else(|| rpc::LookupError::NoObject(id.clone())) } /// As `try_decode`, but take a slice of bytes. pub(crate) fn from_bytes(bytes: &[u8]) -> Option { use tor_bytes::Reader; let mut r = Reader::from_slice(bytes); let x = r.take_u64().ok()?; let ffi_idx = r.take_u64().ok()?; r.should_be_exhausted().ok()?; let ffi_idx = ffi_idx.wrapping_sub(x); Some(GenIdx::from(KeyData::from_ffi(ffi_idx))) } } impl ObjMap { /// Create a new empty ObjMap. pub(crate) fn new() -> Self { Self::default() } /// Unconditionally insert a strong entry for `value` in self, and return its index. pub(crate) fn insert_strong(&mut self, value: Arc) -> GenIdx { self.arena.insert(ObjectRef::Strong(value)) } /// Unconditionally insert a weak entry for `value` in self, and return its index. pub(crate) fn insert_weak(&mut self, value: &Arc) -> GenIdx { self.arena.insert(ObjectRef::Weak(Arc::downgrade(value))) } /// Return the entry from this ObjMap for `idx`. pub(crate) fn lookup(&self, idx: GenIdx) -> Result, LookupError> { self.arena .get(idx) .ok_or(LookupError::NoObject)? .get() .ok_or(LookupError::Expired) } /// Remove the entry at `idx`. /// /// Return true if anything was removed. pub(crate) fn remove(&mut self, idx: GenIdx) -> bool { self.arena.remove(idx).is_some() } /// Testing only: Assert that every invariant for this structure is met. #[cfg(test)] fn assert_okay(&self) {} } /// A failure from ObjMap::lookup. /// /// (This type is immediately returned into rpc::LookupError before we return it.) #[derive(Clone, Debug, thiserror::Error)] pub(crate) enum LookupError { /// There was no object with the given ID. #[error("Object not found")] NoObject, /// The object was present, but it was a weak reference that expired. #[error("Object expired")] Expired, } impl LookupError { /// Convert this `LookupError` into an [`rpc::LookupError`] pub(crate) fn to_rpc_lookup_error(&self, id: rpc::ObjectId) -> rpc::LookupError { match self { LookupError::NoObject => rpc::LookupError::NoObject(id), LookupError::Expired => rpc::LookupError::Expired(id), } } } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_time_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use super::*; use derive_deftly::Deftly; use tor_rpcbase::templates::*; #[derive(Clone, Debug, Deftly)] #[derive_deftly(Object)] struct ExampleObject(#[allow(unused)] String); #[test] fn map_basics() { // Insert an object, make sure it gets inserted twice, and look it up. let obj1 = Arc::new(ExampleObject("abcdef".to_string())); let mut map = ObjMap::new(); map.assert_okay(); let id1 = map.insert_strong(obj1.clone()); let id2 = map.insert_strong(obj1.clone()); assert_ne!(id1, id2); let obj1: Arc = obj1; let obj_out1 = map.lookup(id1).unwrap(); let obj_out2 = map.lookup(id2).unwrap(); assert!(Arc::ptr_eq(&obj1, &obj_out1)); assert!(Arc::ptr_eq(&obj1, &obj_out2)); map.assert_okay(); map.remove(id1); assert!(map.lookup(id1).is_err()); let obj_out2b = map.lookup(id2).unwrap(); assert!(Arc::ptr_eq(&obj_out2, &obj_out2b)); map.assert_okay(); } #[test] fn strong_and_weak() { // Make sure that a strong object behaves like one, and so does a weak // object. let obj1: Arc = Arc::new(ExampleObject("hello".to_string())); let obj2: Arc = Arc::new(ExampleObject("world".to_string())); let mut map = ObjMap::new(); let id1 = map.insert_strong(obj1.clone()); let id2 = map.insert_weak(&obj2); { let out1 = map.lookup(id1).unwrap(); let out2 = map.lookup(id2).unwrap(); assert!(Arc::ptr_eq(&obj1, &out1)); assert!(Arc::ptr_eq(&obj2, &out2)); } map.assert_okay(); // Now drop every object we've got, and see what we can still find. drop(obj1); drop(obj2); { let out1 = map.lookup(id1); let out2 = map.lookup(id2); // This one was strong, so it is still there. assert!(out1.is_ok()); // This one is weak so it went away. assert!(out2.is_err()); } map.assert_okay(); } #[test] fn remove() { // Make sure that removing an object makes it go away. let obj1: Arc = Arc::new(ExampleObject("hello".to_string())); let obj2: Arc = Arc::new(ExampleObject("world".to_string())); let mut map = ObjMap::new(); let id1 = map.insert_strong(obj1.clone()); let id2 = map.insert_weak(&obj2); map.assert_okay(); map.remove(id1); map.assert_okay(); assert!(map.lookup(id1).is_err()); assert!(map.lookup(id2).is_ok()); map.remove(id2); map.assert_okay(); assert!(map.lookup(id1).is_err()); assert!(map.lookup(id2).is_err()); } #[test] fn duplicates() { let obj1: Arc = Arc::new(ExampleObject("hello".to_string())); let obj2: Arc = Arc::new(ExampleObject("world".to_string())); let mut map = ObjMap::new(); let id1 = map.insert_strong(obj1.clone()); let id2 = map.insert_weak(&obj2); { assert_ne!(id2, map.insert_weak(&obj1)); assert_ne!(id2, map.insert_weak(&obj2)); } { assert_ne!(id1, map.insert_strong(obj1.clone())); assert_ne!(id2, map.insert_strong(obj2.clone())); } } #[test] fn objid_encoding() { fn test_roundtrip(a: u32, b: u32, rng: &mut tor_basic_utils::test_rng::TestingRng) { let a: u64 = a.into(); let b: u64 = b.into(); let data = KeyData::from_ffi((a << 33) | (1_u64 << 32) | b); let idx = GenIdx::from(data); let s1 = idx.encode_with_rng(rng); let s2 = idx.encode_with_rng(rng); assert_ne!(s1, s2); assert_eq!(idx, GenIdx::try_decode(&s1).unwrap()); assert_eq!(idx, GenIdx::try_decode(&s2).unwrap()); } let mut rng = tor_basic_utils::test_rng::testing_rng(); test_roundtrip(0, 1, &mut rng); test_roundtrip(0, 2, &mut rng); test_roundtrip(1, 1, &mut rng); test_roundtrip(0xffffffff, 0xffffffff, &mut rng); for _ in 0..256 { test_roundtrip(rng.random(), rng.random(), &mut rng); } } }