Lines
86.21 %
Functions
50 %
Branches
100 %
//! 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 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<dyn rpc::Object>),
/// A weak reference reference.
Weak(Weak<dyn rpc::Object>),
impl ObjectRef {
/// Return this reference as an Arc, if it is present.
fn get(&self) -> Option<Arc<dyn rpc::Object>> {
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<GenIdx, ObjectRef>,
/// 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<R: rand::RngCore>(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<R: rand::RngCore>(self, rng: &mut R) -> [u8; Self::BYTE_LEN] {
use rand::Rng;
use tor_bytes::Writer;
let ffi_idx = self.data().as_ffi();
let x = rng.random::<u64>();
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<Self, rpc::LookupError> {
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<Self> {
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<dyn rpc::Object>) -> 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<dyn rpc::Object>) -> 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<Arc<dyn rpc::Object>, 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 {
LookupError::NoObject => rpc::LookupError::NoObject(id),
LookupError::Expired => rpc::LookupError::Expired(id),
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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
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<dyn rpc::Object> = 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.remove(id1);
assert!(map.lookup(id1).is_err());
let obj_out2b = map.lookup(id2).unwrap();
assert!(Arc::ptr_eq(&obj_out2, &obj_out2b));
fn strong_and_weak() {
// Make sure that a strong object behaves like one, and so does a weak
// object.
let obj1: Arc<dyn rpc::Object> = Arc::new(ExampleObject("hello".to_string()));
let obj2: Arc<dyn rpc::Object> = Arc::new(ExampleObject("world".to_string()));
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));
// 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());
fn remove() {
// Make sure that removing an object makes it go away.
assert!(map.lookup(id2).is_ok());
map.remove(id2);
assert!(map.lookup(id2).is_err());
fn duplicates() {
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()));
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);