use std::sync::Arc;
use crate::bytecode::{Cap, CapRights, NativeIdx, NativeMask, RevocationCell, Value};
use super::fault::Fault;
pub type NativeResult = Result<Value, Fault>;
pub type NativeFn = Arc<dyn Fn(&[Value]) -> NativeResult + Send + Sync>;
pub struct NativeTable {
entries: Vec<Option<(String, NativeFn)>>,
}
impl NativeTable {
pub fn builder() -> NativeTableBuilder {
NativeTableBuilder {
entries: Vec::new(),
}
}
pub fn empty() -> Arc<NativeTable> {
Arc::new(NativeTable {
entries: Vec::new(),
})
}
#[inline]
pub fn get(&self, index: u32) -> Option<&NativeFn> {
self.entries
.get(index as usize)
.and_then(|slot| slot.as_ref())
.map(|(_, f)| f)
}
pub fn index_of(&self, name: &str) -> Option<u32> {
self.entries
.iter()
.enumerate()
.find(|(_, slot)| slot.as_ref().is_some_and(|(n, _)| n == name))
.map(|(i, _)| i as u32)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries
.iter()
.filter_map(|slot| slot.as_ref().map(|(n, _)| n.as_str()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NativeTableError {
DuplicateName(String),
SlotOccupied { index: u32, name: String },
}
impl std::fmt::Display for NativeTableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NativeTableError::DuplicateName(name) => {
write!(f, "duplicate native function registered: '{name}'")
}
NativeTableError::SlotOccupied { index, name } => {
write!(f, "native slot {index} already occupied (registering '{name}')")
}
}
}
}
impl std::error::Error for NativeTableError {}
pub struct NativeTableBuilder {
entries: Vec<Option<(String, NativeFn)>>,
}
impl NativeTableBuilder {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn register<F>(self, name: impl Into<String>, f: F) -> Result<Self, NativeTableError>
where
F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
{
let index = self.entries.len() as u32;
self.register_at(index, name, f)
}
pub fn register_at<F>(
mut self,
index: u32,
name: impl Into<String>,
f: F,
) -> Result<Self, NativeTableError>
where
F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
{
let name = name.into();
for (n, _) in self.entries.iter().flatten() {
if n == &name {
return Err(NativeTableError::DuplicateName(name));
}
}
let index_usize = index as usize;
if index_usize >= self.entries.len() {
self.entries.resize_with(index_usize + 1, || None);
}
if self.entries[index_usize].is_some() {
return Err(NativeTableError::SlotOccupied { index, name });
}
self.entries[index_usize] = Some((name, Arc::new(f)));
Ok(self)
}
pub fn build(self) -> Arc<NativeTable> {
Arc::new(NativeTable {
entries: self.entries,
})
}
}
impl Default for NativeTableBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn expect_arg<'a>(
args: &'a [Value],
index: usize,
fn_name: &str,
) -> Result<&'a Value, Fault> {
args.get(index).ok_or(Fault::NativeError(format!(
"{fn_name}: missing argument {index}"
)))
}
pub fn expect_int(args: &[Value], index: usize, fn_name: &str) -> Result<i64, Fault> {
expect_arg(args, index, fn_name)?
.as_int()
.ok_or(Fault::NativeError(format!(
"{fn_name}: argument {index} is not an int"
)))
}
pub fn expect_bool(args: &[Value], index: usize, fn_name: &str) -> Result<bool, Fault> {
match expect_arg(args, index, fn_name)? {
Value::Bool(b) => Ok(*b),
Value::Int(i) => Ok(*i != 0),
other => Err(Fault::NativeError(format!(
"{fn_name}: argument {index} is not a bool/int (got {})",
other.type_name()
))),
}
}
pub fn expect_message(
args: &[Value],
index: usize,
fn_name: &str,
) -> Result<crate::Message, Fault> {
expect_arg(args, index, fn_name)?
.as_message()
.cloned()
.ok_or(Fault::NativeError(format!(
"{fn_name}: argument {index} is not a message"
)))
}
pub fn expect_u64(args: &[Value], index: usize, fn_name: &str) -> Result<u64, Fault> {
match expect_arg(args, index, fn_name)? {
Value::Pid(p) => Ok(*p),
Value::Int(i) if *i >= 0 => Ok(*i as u64),
Value::Bool(b) => Ok(u64::from(*b)),
other => Err(Fault::NativeError(format!(
"{fn_name}: argument {index} is not a non-negative int/pid (got {})",
other.type_name()
))),
}
}
#[derive(Clone, Debug)]
pub struct NativeGate {
pub has_native: bool,
pub mask: NativeMask,
pub authority_epoch: u64,
pub flow_cell: Arc<RevocationCell>,
pub native_epoch: u64,
pub native_cell: Arc<RevocationCell>,
}
impl NativeGate {
pub fn deny(native_count: usize) -> Self {
Self {
has_native: false,
mask: NativeMask::empty(native_count),
authority_epoch: 0,
flow_cell: Arc::new(RevocationCell::new()),
native_epoch: 0,
native_cell: Arc::new(RevocationCell::new()),
}
}
pub fn from_authority(
cap: &Cap,
flow_cell: Arc<RevocationCell>,
native_cell: Arc<RevocationCell>,
native_count: usize,
) -> Self {
let mask = match &cap.native_mask {
Some(m) => m.clone(),
None => NativeMask::empty(native_count),
};
Self {
has_native: cap.rights.contains(CapRights::NATIVE),
mask,
authority_epoch: cap.epoch(),
flow_cell,
native_epoch: native_cell.epoch(),
native_cell,
}
}
pub fn is_live(&self) -> bool {
self.flow_cell.epoch() == self.authority_epoch
&& self.native_cell.epoch() == self.native_epoch
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeCallError {
NoNativeRight,
IndexNotAllowlisted(NativeIdx),
IndexOutOfRange(NativeIdx),
Revoked,
}
impl std::fmt::Display for NativeCallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NativeCallError::NoNativeRight => f.write_str("CALL_NATIVE: flow lacks NATIVE right"),
NativeCallError::IndexNotAllowlisted(idx) => {
write!(f, "CALL_NATIVE: index {idx} not on allowlist")
}
NativeCallError::IndexOutOfRange(idx) => {
write!(f, "CALL_NATIVE: index {idx} out of range")
}
NativeCallError::Revoked => f.write_str("CALL_NATIVE: capability revoked"),
}
}
}
impl std::error::Error for NativeCallError {}
pub fn check_native_call(
cap: &Cap,
table: &NativeTable,
idx: NativeIdx,
) -> Result<(), NativeCallError> {
if (idx as usize) >= table.len() {
return Err(NativeCallError::IndexOutOfRange(idx));
}
if !cap.rights.contains(CapRights::NATIVE) {
return Err(NativeCallError::NoNativeRight);
}
match &cap.native_mask {
Some(mask) if mask.allows(idx) => Ok(()),
_ => Err(NativeCallError::IndexNotAllowlisted(idx)),
}
}
pub fn check_native_gate(
gate: &NativeGate,
table: &NativeTable,
idx: NativeIdx,
) -> Result<(), NativeCallError> {
if (idx as usize) >= table.len() {
return Err(NativeCallError::IndexOutOfRange(idx));
}
if !gate.is_live() {
return Err(NativeCallError::Revoked);
}
if !gate.has_native {
return Err(NativeCallError::NoNativeRight);
}
if gate.mask.allows(idx) {
Ok(())
} else {
Err(NativeCallError::IndexNotAllowlisted(idx))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_at_leaves_holes_as_none() -> Result<(), Box<dyn std::error::Error>> {
let table = NativeTable::builder()
.register_at(10, "answer", |_| Ok(Value::Int(42)))?
.build();
assert_eq!(table.len(), 11);
assert_eq!(table.index_of("answer"), Some(10));
let f = table.get(10).ok_or("missing native")?;
assert!(matches!(f(&[])?, Value::Int(42)));
assert!(table.get(2).is_none());
Ok(())
}
#[test]
fn register_at_errors_on_duplicate_slot() {
let result = NativeTable::builder()
.register_at(3, "a", |_| Ok(Value::Unit))
.and_then(|b| b.register_at(3, "b", |_| Ok(Value::Unit)));
assert!(matches!(
result,
Err(NativeTableError::SlotOccupied { index: 3, .. })
));
}
#[test]
fn register_errors_on_duplicate_name() {
let result = NativeTable::builder()
.register("x", |_| Ok(Value::Unit))
.and_then(|b| b.register("x", |_| Ok(Value::Unit)));
assert!(matches!(result, Err(NativeTableError::DuplicateName(_))));
}
#[test]
fn denies_unlisted_index_even_with_native_right() {
use crate::bytecode::{CapTarget, RevocationCell};
let cell = RevocationCell::new();
let mask = NativeMask::from_indices(16, &[2, 4]);
let cap = Cap::root(CapTarget::Flow(1), CapRights::NATIVE, Some(mask), &cell);
let table = NativeTable {
entries: Vec::new(),
};
assert!(matches!(
check_native_call(&cap, &table, 4),
Err(NativeCallError::IndexOutOfRange(_))
));
}
}