#![doc(html_root_url = "https://docs.rs/ic-cose-types/latest")]
#![allow(clippy::needless_doctest_main)]
use candid::{utils::ArgumentEncoder, CandidType, Principal};
use ciborium::into_writer;
use serde::Serialize;
use std::{collections::BTreeSet, future::Future};
pub mod cose;
pub mod types;
pub use cose::format_error;
pub static ANONYMOUS: Principal = Principal::anonymous();
pub const MILLISECONDS: u64 = 1_000_000u64;
pub fn to_cbor_bytes(obj: &impl Serialize) -> Vec<u8> {
try_to_cbor_bytes(obj).expect("failed to encode in CBOR format")
}
pub fn try_to_cbor_bytes(obj: &impl Serialize) -> Result<Vec<u8>, String> {
let mut buf: Vec<u8> = Vec::new();
into_writer(obj, &mut buf)
.map_err(|err| format!("failed to encode in CBOR format: {err:?}"))?;
Ok(buf)
}
pub fn validate_str(s: &str) -> Result<(), String> {
if s.is_empty() {
return Err("empty string".to_string());
}
if s.len() > 64 {
return Err("string length exceeds the limit 64".to_string());
}
for c in s.chars() {
if !matches!(c, 'a'..='z' | '0'..='9' | '_' ) {
return Err(format!("invalid character: {}", c));
}
}
Ok(())
}
pub fn validate_principals(principals: &BTreeSet<Principal>) -> Result<(), String> {
if principals.is_empty() {
return Err("principals cannot be empty".to_string());
}
validate_principals_not_anonymous(principals)
}
pub fn validate_principals_not_anonymous(principals: &BTreeSet<Principal>) -> Result<(), String> {
if principals.contains(&ANONYMOUS) {
return Err("anonymous user is not allowed".to_string());
}
Ok(())
}
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub trait CanisterCaller: Sized {
fn canister_query<
In: ArgumentEncoder + Send,
Out: CandidType + for<'a> candid::Deserialize<'a>,
>(
&self,
canister: &Principal,
method: &str,
args: In,
) -> impl Future<Output = Result<Out, BoxError>> + Send;
fn canister_update<
In: ArgumentEncoder + Send,
Out: CandidType + for<'a> candid::Deserialize<'a>,
>(
&self,
canister: &Principal,
method: &str,
args: In,
) -> impl Future<Output = Result<Out, BoxError>> + Send;
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn validate_str_accepts_expected_names() {
assert!(validate_str("_").is_ok());
assert!(validate_str("abc_123").is_ok());
assert!(validate_str(&"a".repeat(64)).is_ok());
}
#[test]
fn validate_str_rejects_invalid_names() {
assert_eq!(validate_str("").unwrap_err(), "empty string");
assert_eq!(
validate_str(&"a".repeat(65)).unwrap_err(),
"string length exceeds the limit 64"
);
assert_eq!(validate_str("ABC").unwrap_err(), "invalid character: A");
assert_eq!(validate_str("abc-123").unwrap_err(), "invalid character: -");
}
#[test]
fn validate_principals_requires_non_empty_set() {
let principals = BTreeSet::new();
assert_eq!(
validate_principals(&principals).unwrap_err(),
"principals cannot be empty"
);
}
#[test]
fn validate_principals_rejects_anonymous() {
let mut principals = BTreeSet::new();
principals.insert(Principal::anonymous());
assert_eq!(
validate_principals(&principals).unwrap_err(),
"anonymous user is not allowed"
);
assert_eq!(
validate_principals_not_anonymous(&principals).unwrap_err(),
"anonymous user is not allowed"
);
}
#[test]
fn validate_principals_accepts_authenticated_principal() {
let mut principals = BTreeSet::new();
principals.insert(Principal::management_canister());
assert!(validate_principals(&principals).is_ok());
assert!(validate_principals_not_anonymous(&principals).is_ok());
assert!(validate_principals_not_anonymous(&BTreeSet::new()).is_ok());
}
}