1use std::collections::BTreeMap;
65use std::panic::AssertUnwindSafe;
66
67use async_trait::async_trait;
68use base64::Engine;
69use base64::engine::general_purpose::URL_SAFE_NO_PAD;
70use cratefield_core::{
71 ActorError, ActorHandlers, ActorStore, ActorWrites, Actors, HarnessConfig, MAX_ACTOR_KEY_BYTES,
72 MAX_ACTOR_KIND_BYTES, MAX_ACTOR_MESSAGE_BYTES, check_actor_call, run_actor_alarm,
73 run_actor_message, validate_actor_key, validate_actor_kind,
74};
75use futures_util::lock::Mutex;
76use hmac::{Hmac, KeyInit, Mac};
77use serde::de::DeserializeOwned;
78use sha2::Sha256;
79use subtle::ConstantTimeEq;
80use time::OffsetDateTime;
81use worker::d1::serde_wasm_bindgen;
82use worker::js_sys::futures::{JsFuture, future_to_promise};
83use worker::js_sys::{Date, Function, Promise, Reflect, Uint8Array};
84use worker::send::{IntoSendFuture, SendWrapper};
85use worker::wasm_bindgen::JsCast;
86use worker::wasm_bindgen::JsValue;
87use worker::wasm_bindgen::closure::Closure;
88use worker::worker_sys::{DurableObjectState, DurableObjectStorage, DurableObjectTransaction};
89use worker::{
90 Env, Headers, ListOptions, Method, ObjectNamespace, Request, RequestInit, Response,
91 Result as WorkerResult, State,
92};
93
94use super::WorkersClock;
95
96type HmacSha256 = Hmac<Sha256>;
97
98const SIGNATURE_HEADER: &str = "x-cratefield-actor-signature";
100
101const KEY_LABEL: &[u8] = b"cratefield actor protocol v1";
104
105const META_KIND_KEY: &str = "\u{0}kind";
108
109const META_KEY_KEY: &str = "\u{0}key";
111
112const USER_PREFIX: &str = "u:";
116
117fn storage_key(key: &str) -> String {
119 format!("{USER_PREFIX}{key}")
120}
121
122const MAX_FRAME_BYTES: usize =
128 1 + MAX_ACTOR_KIND_BYTES + 2 + MAX_ACTOR_KEY_BYTES + MAX_ACTOR_MESSAGE_BYTES;
129
130fn frame_matches_identity(
147 name: Option<&str>,
148 stored: Option<(&str, &str)>,
149 kind: &str,
150 key: &str,
151) -> bool {
152 let named = name.is_none_or(|name| name == format!("{kind}:{key}"));
153 let persisted =
154 stored.is_none_or(|(stored_kind, stored_key)| stored_kind == kind && stored_key == key);
155 named && persisted
156}
157
158fn protocol_key(secret: &[u8]) -> [u8; 32] {
160 hmac_sha256(secret, KEY_LABEL)
161}
162
163fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
165 let mut mac =
166 <HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
167 mac.update(data);
168 let mut out = [0u8; 32];
169 out.copy_from_slice(&mac.finalize().into_bytes());
170 out
171}
172
173fn encode_frame(kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
181 let kind_len = u8::try_from(kind.len()).map_err(|_| {
182 ActorError::TooLarge(format!(
183 "actor kind of {} bytes overflows the frame",
184 kind.len()
185 ))
186 })?;
187 let key_len = u16::try_from(key.len()).map_err(|_| {
188 ActorError::TooLarge(format!(
189 "actor key of {} bytes overflows the frame",
190 key.len()
191 ))
192 })?;
193 let mut frame = Vec::with_capacity(1 + kind.len() + 2 + key.len() + message.len());
194 frame.push(kind_len);
195 frame.extend_from_slice(kind.as_bytes());
196 frame.extend_from_slice(&key_len.to_be_bytes());
197 frame.extend_from_slice(key.as_bytes());
198 frame.extend_from_slice(message);
199 Ok(frame)
200}
201
202fn decode_frame(frame: &[u8]) -> Result<(String, String, Vec<u8>), ActorError> {
211 let mut cursor = frame;
212 let (&kind_len, rest) = cursor.split_first().ok_or_else(malformed)?;
213 cursor = rest;
214 let kind_len = usize::from(kind_len);
215 let (kind, rest) = split_at(cursor, kind_len)?;
216 let (key_len, rest) = split_at(rest, 2)?;
217 let key_len = usize::from(u16::from_be_bytes([key_len[0], key_len[1]]));
218 let (key, message) = split_at(rest, key_len)?;
219 let kind = std::str::from_utf8(kind)
220 .map_err(|_| ActorError::Operation("actor frame kind is not utf-8".to_owned()))?;
221 let key = std::str::from_utf8(key)
222 .map_err(|_| ActorError::Operation("actor frame key is not utf-8".to_owned()))?;
223 Ok((kind.to_owned(), key.to_owned(), message.to_vec()))
224}
225
226fn split_at(bytes: &[u8], at: usize) -> Result<(&[u8], &[u8]), ActorError> {
227 if bytes.len() < at {
228 return Err(malformed());
229 }
230 Ok(bytes.split_at(at))
231}
232
233fn malformed() -> ActorError {
234 ActorError::Operation("actor frame is malformed".to_owned())
235}
236
237fn sign_frame(key: &[u8], frame: &[u8]) -> String {
239 URL_SAFE_NO_PAD.encode(hmac_sha256(key, frame))
240}
241
242fn verify_frame(signature: &str, key: &[u8], frame: &[u8]) -> bool {
248 let Ok(bytes) = URL_SAFE_NO_PAD.decode(signature) else {
249 return false;
250 };
251 let Ok(expected) = <[u8; 32]>::try_from(bytes) else {
252 return false;
253 };
254 bool::from(hmac_sha256(key, frame).ct_eq(&expected))
255}
256
257fn verification_keys(config: &HarnessConfig) -> Vec<[u8; 32]> {
260 let mut keys = vec![protocol_key(config.harness_secret.as_bytes())];
261 if let Some(previous) = &config.harness_secret_previous {
262 keys.push(protocol_key(previous.as_bytes()));
263 }
264 keys
265}
266
267pub struct ActorDriver {
270 state: DurableObjectState,
276 handlers: ActorHandlers,
277 keys: Vec<[u8; 32]>,
279 lock: Mutex<()>,
284}
285
286impl ActorDriver {
287 #[must_use]
293 pub fn new(state: State, env: &Env, handlers: ActorHandlers) -> Self {
294 let keys = match HarnessConfig::from_config(&crate::config::EnvConfig(env.clone())) {
295 Ok(config) => verification_keys(&config),
296 Err(_) => Vec::new(),
297 };
298 Self {
299 state: state._inner(),
300 handlers,
301 keys,
302 lock: Mutex::new(()),
303 }
304 }
305
306 pub async fn fetch(&self, req: Request) -> WorkerResult<Response> {
314 let mut req = req;
315 let Ok(Some(signature)) = req.headers().get(SIGNATURE_HEADER) else {
316 return Response::error("actor request is not signed", 403);
317 };
318 if declared_content_length(&req).is_some_and(|length| length > MAX_FRAME_BYTES) {
323 return Response::error("actor frame is too large", 413);
324 }
325 let frame = req.bytes().await?;
326 if frame.len() > MAX_FRAME_BYTES {
327 return Response::error("actor frame is too large", 413);
328 }
329 if !self
330 .keys
331 .iter()
332 .any(|key| verify_frame(&signature, key, &frame))
333 {
334 return Response::error("actor request signature is invalid", 403);
335 }
336 let Ok((kind, key, message)) = decode_frame(&frame) else {
337 return Response::error("actor frame is malformed", 400);
338 };
339 if validate_actor_kind(&kind).is_err() || validate_actor_key(&key).is_err() {
340 return Response::error("actor kind or key is invalid", 400);
341 }
342 let Some(handler) = self.handlers.get(&kind) else {
343 return Response::error("no handler for this actor kind", 404);
344 };
345
346 let _serialized = self.lock.lock().await;
347 let stored = read_meta(&self.state).await?;
353 let named_identity = self.state.id()?.name();
354 if !frame_matches_identity(
355 named_identity.as_deref(),
356 stored
357 .as_ref()
358 .map(|(kind, key)| (kind.as_str(), key.as_str())),
359 &kind,
360 &key,
361 ) {
362 return Response::error("actor frame names a different actor", 400);
363 }
364 let store = DurableActorStore::new(&self.state, &kind, &key);
365 match run_actor_message(
366 handler.as_ref(),
367 &store,
368 &WorkersClock,
369 &kind,
370 &key,
371 &message,
372 )
373 .await
374 {
375 Ok(reply) => Response::from_bytes(reply),
376 Err(error) => actor_error_response(&error),
377 }
378 }
379
380 pub async fn alarm(&self) -> WorkerResult<Response> {
392 let _serialized = self.lock.lock().await;
393 let Some((kind, key)) = read_meta(&self.state).await? else {
394 return Response::empty();
397 };
398 let Some(handler) = self.handlers.get(&kind) else {
399 return Response::empty();
400 };
401 let store = DurableActorStore::new(&self.state, &kind, &key);
402 match run_actor_alarm(handler.as_ref(), &store, &WorkersClock, &kind, &key).await {
403 Ok(()) => Response::empty(),
404 Err(error) => Err(worker::Error::RustError(error.to_string())),
405 }
406 }
407}
408
409fn declared_content_length(req: &Request) -> Option<usize> {
413 req.headers()
414 .get("content-length")
415 .ok()
416 .flatten()
417 .and_then(|value| value.parse().ok())
418}
419
420async fn read_meta(state: &DurableObjectState) -> WorkerResult<Option<(String, String)>> {
423 let storage = state.storage()?;
424 let kind = storage_get::<String>(&storage, META_KIND_KEY).await?;
425 let key = storage_get::<String>(&storage, META_KEY_KEY).await?;
426 Ok(match (kind, key) {
427 (Some(kind), Some(key)) => Some((kind, key)),
428 _ => None,
429 })
430}
431
432async fn storage_get<T: DeserializeOwned>(
439 storage: &DurableObjectStorage,
440 key: &str,
441) -> WorkerResult<Option<T>> {
442 let value = JsFuture::from(storage.get(key)?).into_send().await?;
443 if value.is_undefined() {
444 return Ok(None);
445 }
446 Ok(Some(serde_wasm_bindgen::from_value(value)?))
447}
448
449async fn storage_list(
453 storage: &DurableObjectStorage,
454 prefix: &str,
455) -> WorkerResult<BTreeMap<String, Vec<u8>>> {
456 let options = ListOptions::new().prefix(prefix);
457 let promise = storage.list_with_options(serde_wasm_bindgen::to_value(&options)?.into())?;
458 let listed = JsFuture::from(promise).into_send().await?;
459 serde_wasm_bindgen::from_value(listed).map_err(Into::into)
460}
461
462async fn storage_alarm(storage: &DurableObjectStorage) -> WorkerResult<Option<i64>> {
465 let value = JsFuture::from(storage.get_alarm(JsValue::NULL.into())?)
466 .into_send()
467 .await?;
468 #[allow(clippy::cast_possible_truncation)]
471 Ok(value.as_f64().map(|ms| ms as i64))
472}
473
474fn actor_error_response(error: &ActorError) -> WorkerResult<Response> {
477 let (status, detail) = match error {
478 ActorError::TooLarge(detail) => (413, detail.clone()),
479 ActorError::Handler(detail) => (422, detail.clone()),
480 ActorError::NotConfigured => (404, "no handler for this actor kind".to_owned()),
481 ActorError::Timeout | ActorError::Operation(_) => (500, error.to_string()),
482 };
483 Response::error(detail, status)
484}
485
486fn store_error(error: &worker::Error) -> ActorError {
487 ActorError::Operation(error.to_string())
488}
489
490fn js_error(error: JsValue) -> ActorError {
493 store_error(&worker::Error::from(error))
494}
495
496struct DurableActorStore<'a> {
501 state: &'a DurableObjectState,
502 kind: String,
503 key: String,
504}
505
506impl<'a> DurableActorStore<'a> {
507 fn new(state: &'a DurableObjectState, kind: &str, key: &str) -> Self {
508 Self {
509 state,
510 kind: kind.to_owned(),
511 key: key.to_owned(),
512 }
513 }
514
515 fn storage(&self) -> Result<DurableObjectStorage, ActorError> {
517 self.state.storage().map_err(js_error)
518 }
519}
520
521#[async_trait]
522impl ActorStore for DurableActorStore<'_> {
523 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
524 let storage = self.storage()?;
525 storage_get::<Vec<u8>>(&storage, &storage_key(key))
526 .await
527 .map_err(|error| store_error(&error))
528 }
529
530 async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
531 let storage = self.storage()?;
532 let entries = storage_list(&storage, &storage_key(prefix))
536 .await
537 .map_err(|error| store_error(&error))?;
538 Ok(entries
539 .into_iter()
540 .filter_map(|(key, value)| {
541 key.strip_prefix(USER_PREFIX)
542 .map(|key| (key.to_owned(), value))
543 })
544 .collect())
545 }
546
547 async fn alarm(&self) -> Result<Option<OffsetDateTime>, ActorError> {
548 let storage = self.storage()?;
549 match storage_alarm(&storage)
550 .await
551 .map_err(|error| store_error(&error))?
552 {
553 Some(ms) => Ok(Some(ms_to_alarm(ms)?)),
554 None => Ok(None),
555 }
556 }
557
558 async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError> {
559 let ActorWrites {
560 clear_all,
561 entries,
562 alarm,
563 } = writes;
564 let kind = self.kind.clone();
565 let key = self.key.clone();
566 let entries: Vec<(String, Option<Vec<u8>>)> = entries.into_iter().collect();
567
568 let clearing: Vec<String> = if clear_all {
575 let storage = self.storage()?;
576 storage_list(&storage, USER_PREFIX)
577 .await
578 .map_err(|error| store_error(&error))?
579 .into_keys()
580 .collect()
581 } else {
582 Vec::new()
583 };
584
585 let storage = self.storage()?;
598 let closure: Box<dyn FnOnce(DurableObjectTransaction) -> Promise> =
599 Box::new(move |tx: DurableObjectTransaction| -> Promise {
600 future_to_promise(AssertUnwindSafe(async move {
601 apply_writes(tx, clearing, entries, kind, key, alarm).await
602 }))
603 });
604 let closure = SendWrapper::new(Closure::once_assert_unwind_safe(closure));
609 JsFuture::from(storage.transaction(&closure).map_err(js_error)?)
610 .into_send()
611 .await
612 .map_err(js_error)?;
613 Ok(())
614 }
615}
616
617#[allow(clippy::option_option)]
625async fn apply_writes(
626 tx: DurableObjectTransaction,
627 clearing: Vec<String>,
628 entries: Vec<(String, Option<Vec<u8>>)>,
629 kind: String,
630 key: String,
631 alarm: Option<Option<OffsetDateTime>>,
632) -> Result<JsValue, JsValue> {
633 for stored in clearing {
634 JsFuture::from(tx.delete(&stored)?).await?;
635 }
636 for (entry_key, value) in entries {
637 let stored = storage_key(&entry_key);
638 match value {
639 Some(bytes) => {
640 let value = serde_wasm_bindgen::to_value(&bytes)?;
641 JsFuture::from(tx.put(&stored, value)?).await?;
642 }
643 None => {
644 JsFuture::from(tx.delete(&stored)?).await?;
645 }
646 }
647 }
648 JsFuture::from(tx.put(META_KIND_KEY, serde_wasm_bindgen::to_value(&kind)?)?).await?;
651 JsFuture::from(tx.put(META_KEY_KEY, serde_wasm_bindgen::to_value(&key)?)?).await?;
652
653 if let Some(alarm) = alarm {
654 let this: &JsValue = tx.as_ref();
655 if let Some(at) = alarm {
656 #[allow(clippy::cast_precision_loss)]
659 let when = Date::new(&JsValue::from_f64(alarm_epoch_ms(at) as f64));
660 let set = transaction_method(&tx, "setAlarm")?;
661 let promise: Promise = set.call2(this, &when, &JsValue::NULL)?.unchecked_into();
662 JsFuture::from(promise).await?;
663 } else {
664 let delete = transaction_method(&tx, "deleteAlarm")?;
665 let promise: Promise = delete.call1(this, &JsValue::NULL)?.unchecked_into();
666 JsFuture::from(promise).await?;
667 }
668 }
669 Ok(JsValue::UNDEFINED)
670}
671
672fn transaction_method(tx: &DurableObjectTransaction, name: &str) -> Result<Function, JsValue> {
676 let object: &JsValue = tx.as_ref();
677 Reflect::get(object, &JsValue::from_str(name))?.dyn_into()
678}
679
680fn ms_to_alarm(ms: i64) -> Result<OffsetDateTime, ActorError> {
683 OffsetDateTime::from_unix_timestamp_nanos(i128::from(ms) * 1_000_000)
684 .map_err(|error| ActorError::Operation(format!("stored alarm is not a time: {error}")))
685}
686
687fn alarm_epoch_ms(at: OffsetDateTime) -> i64 {
690 let nanos = at.unix_timestamp_nanos();
691 let ms = nanos / 1_000_000;
692 i64::try_from(ms).unwrap_or(if ms.is_negative() { i64::MIN } else { i64::MAX })
695}
696
697pub(crate) struct DurableActors {
700 namespace: ObjectNamespace,
701 signing_key: Vec<u8>,
702}
703
704impl DurableActors {
705 pub(crate) fn new(namespace: ObjectNamespace, config: &HarnessConfig) -> Self {
708 Self {
709 namespace,
710 signing_key: protocol_key(config.harness_secret.as_bytes()).to_vec(),
711 }
712 }
713}
714
715#[async_trait]
716impl Actors for DurableActors {
717 async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
718 check_actor_call(kind, key, message)?;
719 if self.signing_key.is_empty() {
720 return Err(ActorError::Operation(
721 "actor host has no signing key: HARNESS_SECRET is not configured".to_owned(),
722 ));
723 }
724
725 let frame = encode_frame(kind, key, message)?;
726 let signature = sign_frame(&self.signing_key, &frame);
727 let mut init = RequestInit::new();
728 init.method = Method::Post;
729 let headers = Headers::new();
730 headers
731 .set(SIGNATURE_HEADER, &signature)
732 .map_err(|error| store_error(&error))?;
733 init.headers = headers;
734 init.with_body(Some(Uint8Array::from(frame.as_slice()).into()));
735
736 let request = Request::new_with_init("https://actors.invalid/", &init)
740 .map_err(|error| store_error(&error))?;
741 let stub = self
742 .namespace
743 .id_from_name(&format!("{kind}:{key}"))
744 .map_err(|error| store_error(&error))?
745 .get_stub()
746 .map_err(|error| store_error(&error))?;
747 let mut response = stub
748 .fetch_with_request(request)
749 .into_send()
750 .await
751 .map_err(|error| store_error(&error))?;
752
753 match response.status_code() {
754 200 => response
755 .bytes()
756 .into_send()
757 .await
758 .map_err(|error| store_error(&error)),
759 404 => Err(ActorError::NotConfigured),
760 413 => Err(ActorError::TooLarge(response_text(&mut response).await)),
761 422 => Err(ActorError::Handler(response_text(&mut response).await)),
762 status => Err(ActorError::Operation(format!(
763 "actor host answered {status}: {}",
764 response_text(&mut response).await
765 ))),
766 }
767 }
768}
769
770async fn response_text(response: &mut Response) -> String {
771 response.text().into_send().await.unwrap_or_default()
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 fn keys() -> (HarnessConfig, [u8; 32], Option<[u8; 32]>) {
780 let config = HarnessConfig {
781 harness_secret: "test-secret-0123456789abcdef-0123".to_owned(),
782 harness_secret_previous: Some("previous-secret-0123456789abcdef".to_owned()),
783 harness_secret_revoked: Vec::new(),
784 harness_venture: None,
785 admin_token: None,
786 env: cratefield_core::VentureEnv::Development,
787 };
788 let current = protocol_key(config.harness_secret.as_bytes());
789 let previous = config
790 .harness_secret_previous
791 .as_ref()
792 .map(|secret| protocol_key(secret.as_bytes()));
793 (config, current, previous)
794 }
795
796 #[test]
797 fn a_frame_round_trips() {
798 let frame = encode_frame("counter", "user-7", b"inc").expect("fits");
799 assert_eq!(
800 decode_frame(&frame).expect("decodes"),
801 ("counter".to_owned(), "user-7".to_owned(), b"inc".to_vec())
802 );
803 let frame = encode_frame("counter", "k", b"").expect("fits");
805 assert_eq!(decode_frame(&frame).expect("decodes").2, Vec::<u8>::new());
806 }
807
808 #[test]
809 fn a_malformed_frame_is_refused() {
810 assert!(decode_frame(&[4, b'a']).is_err());
812 assert!(decode_frame(&[1, b'a', 0, 9, b'k']).is_err());
814 assert!(decode_frame(&[]).is_err());
816 assert!(decode_frame(&[1, 0xff, 0, 1, b'k']).is_err());
818 }
819
820 #[test]
821 fn a_signature_verifies_and_a_tampered_frame_does_not() {
822 let (_, current, _) = keys();
823 let frame = encode_frame("counter", "k", b"inc").expect("fits");
824 let signature = sign_frame(¤t, &frame);
825 assert!(verify_frame(&signature, ¤t, &frame));
826
827 let mut tampered = frame.clone();
828 tampered[0] = tampered[0].wrapping_add(1);
829 assert!(!verify_frame(&signature, ¤t, &tampered));
830 }
831
832 #[test]
833 fn a_signature_over_a_different_kind_or_key_is_refused() {
834 let (_, current, _) = keys();
835 let signature = sign_frame(
836 ¤t,
837 &encode_frame("counter", "one", b"m").expect("fits"),
838 );
839 assert!(!verify_frame(
840 &signature,
841 ¤t,
842 &encode_frame("counter", "two", b"m").expect("fits")
843 ));
844 assert!(!verify_frame(
845 &signature,
846 ¤t,
847 &encode_frame("other", "one", b"m").expect("fits")
848 ));
849 }
850
851 #[test]
852 fn a_wrong_secret_is_refused_and_the_previous_one_is_accepted() {
853 let (config, current, previous) = keys();
854 let frame = encode_frame("counter", "k", b"inc").expect("fits");
855
856 let previous = previous.expect("the fixture sets one");
857 let previous_signature = sign_frame(&previous, &frame);
858 assert!(!verify_frame(&previous_signature, ¤t, &frame));
859 let accepted = verification_keys(&config)
861 .iter()
862 .any(|key| verify_frame(&previous_signature, key, &frame));
863 assert!(accepted, "the previous secret was not accepted");
864
865 let other = protocol_key(b"a secret nobody configured");
866 let signature = sign_frame(&other, &frame);
867 assert!(
868 !verification_keys(&config)
869 .iter()
870 .any(|key| verify_frame(&signature, key, &frame))
871 );
872 }
873
874 #[test]
875 fn a_malformed_signature_is_refused() {
876 let (_, current, _) = keys();
877 let frame = encode_frame("counter", "k", b"inc").expect("fits");
878 assert!(!verify_frame("not base64!", ¤t, &frame));
879 assert!(!verify_frame("", ¤t, &frame));
880 assert!(!verify_frame(
882 &URL_SAFE_NO_PAD.encode(b"short"),
883 ¤t,
884 &frame
885 ));
886 }
887
888 #[test]
889 fn the_protocol_key_is_domain_separated_from_the_secret() {
890 let (_, current, _) = keys();
893 assert_ne!(¤t[..], b"test-secret-0123456789abcdef-0123");
894 }
895
896 #[test]
897 fn an_alarm_round_trips_through_epoch_milliseconds() {
898 let now = OffsetDateTime::UNIX_EPOCH;
899 assert_eq!(
900 alarm_epoch_ms(now + time::Duration::milliseconds(1500)),
901 1500
902 );
903 assert_eq!(
904 ms_to_alarm(1500).expect("a time"),
905 now + time::Duration::milliseconds(1500)
906 );
907 assert_eq!(alarm_epoch_ms(now), 0);
908 }
909
910 #[test]
911 fn a_frame_matches_its_identity() {
912 assert!(frame_matches_identity(
915 Some("counter:k"),
916 None,
917 "counter",
918 "k"
919 ));
920 assert!(!frame_matches_identity(
921 Some("counter:other"),
922 None,
923 "counter",
924 "k"
925 ));
926 assert!(!frame_matches_identity(
927 Some("other:k"),
928 None,
929 "counter",
930 "k"
931 ));
932 assert!(frame_matches_identity(None, None, "counter", "k"));
935
936 assert!(frame_matches_identity(
938 None,
939 Some(("counter", "k")),
940 "counter",
941 "k"
942 ));
943 assert!(!frame_matches_identity(
944 None,
945 Some(("counter", "other")),
946 "counter",
947 "k"
948 ));
949 assert!(!frame_matches_identity(
950 None,
951 Some(("other", "k")),
952 "counter",
953 "k"
954 ));
955 assert!(!frame_matches_identity(
957 Some("counter:k"),
958 Some(("counter", "other")),
959 "counter",
960 "k"
961 ));
962 }
963}