224 lines
7.2 KiB
Rust
224 lines
7.2 KiB
Rust
//! Selector resolution — matches a request's role selector against the role table.
|
|
//!
|
|
//! Port of `selector.c`.
|
|
|
|
use crate::role_table::RoleTable;
|
|
|
|
// ── Error Codes ──────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SelectorError {
|
|
Ok = 0,
|
|
Ambiguous = -1,
|
|
NotFound = -2,
|
|
NoDefault = -3,
|
|
PathMismatch = -4,
|
|
NostrIndexDeprecated = -5,
|
|
IndexDeprecated = -6,
|
|
RoleRequired = -7,
|
|
PathRequired = -8,
|
|
}
|
|
|
|
impl SelectorError {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Ok => "ok",
|
|
Self::Ambiguous => "ambiguous_role_selector",
|
|
Self::NotFound => "unknown_role",
|
|
Self::NoDefault => "no_default_role",
|
|
Self::PathMismatch => "path_not_allowed",
|
|
Self::NostrIndexDeprecated => "nostr_index_deprecated",
|
|
Self::IndexDeprecated => "index_deprecated",
|
|
Self::RoleRequired => "role_required",
|
|
Self::PathRequired => "path_required",
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Selector Request ─────────────────────────────────────────────────────────
|
|
|
|
/// Parsed selector from a request's options object.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SelectorRequest {
|
|
pub has_role: bool,
|
|
pub role_name: String,
|
|
|
|
pub has_nostr_index: bool,
|
|
pub nostr_index: i32,
|
|
|
|
pub has_role_path: bool,
|
|
pub role_path: String,
|
|
|
|
pub has_index: bool,
|
|
pub index: i32,
|
|
}
|
|
|
|
impl SelectorRequest {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
}
|
|
|
|
// ── Resolution ───────────────────────────────────────────────────────────────
|
|
|
|
/// Resolve a selector request against the role table.
|
|
///
|
|
/// On success returns `Ok(role_index)` — the index into the table.
|
|
/// On failure returns the appropriate `SelectorError`.
|
|
pub fn selector_resolve(
|
|
req: &SelectorRequest,
|
|
table: &RoleTable,
|
|
) -> Result<usize, SelectorError> {
|
|
// Both role and role_path are required together (combined selector).
|
|
// No resolution order and no default role.
|
|
if !req.has_role && !req.has_nostr_index && !req.has_role_path {
|
|
// No selector given — check for default "main" role
|
|
if table.get_default().is_some() {
|
|
// Find the index of "main"
|
|
return Ok(table
|
|
.entries
|
|
.iter()
|
|
.position(|e| e.name == "main")
|
|
.ok_or(SelectorError::NoDefault)?);
|
|
}
|
|
return Err(SelectorError::NoDefault);
|
|
}
|
|
|
|
if req.has_role {
|
|
if !req.has_role_path {
|
|
// role without role_path — check if it's a fixed-path role
|
|
let entry = table.find_by_name(&req.role_name).ok_or(SelectorError::NotFound)?;
|
|
if entry.has_variable_path() {
|
|
// Variable path role needs role_path
|
|
return Err(SelectorError::PathRequired);
|
|
}
|
|
// Fixed path role — OK, return index
|
|
return Ok(table
|
|
.entries
|
|
.iter()
|
|
.position(|e| e.name == req.role_name)
|
|
.ok_or(SelectorError::NotFound)?);
|
|
}
|
|
|
|
// role + role_path: verify path matches the role's template
|
|
let entry = table.find_by_name(&req.role_name).ok_or(SelectorError::NotFound)?;
|
|
if !entry.path_matches_with_range(&req.role_path) {
|
|
return Err(SelectorError::PathMismatch);
|
|
}
|
|
return Ok(table
|
|
.entries
|
|
.iter()
|
|
.position(|e| e.name == req.role_name)
|
|
.ok_or(SelectorError::NotFound)?);
|
|
}
|
|
|
|
if req.has_nostr_index {
|
|
// nostr_index is deprecated — must use role + role_path
|
|
return Err(SelectorError::NostrIndexDeprecated);
|
|
}
|
|
|
|
if req.has_role_path && !req.has_role {
|
|
return Err(SelectorError::RoleRequired);
|
|
}
|
|
|
|
Err(SelectorError::NotFound)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::role_table::*;
|
|
|
|
fn make_test_table() -> RoleTable {
|
|
let mut table = RoleTable::new();
|
|
// main: fixed path
|
|
table.register_role_path(
|
|
"main",
|
|
"m/44'/1237'/0'/0/0",
|
|
RolePurpose::Nostr,
|
|
RoleCurve::Secp256k1,
|
|
-1, -1, -1, &[],
|
|
).unwrap();
|
|
// nostr_range: variable path with range 0-3
|
|
table.register_role_path(
|
|
"nostr_range",
|
|
"m/44'/1237'/%d'/0/0",
|
|
RolePurpose::Nostr,
|
|
RoleCurve::Secp256k1,
|
|
0, 3, -1, &[],
|
|
).unwrap();
|
|
table
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_fixed_path() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role = true;
|
|
req.role_name = "main".to_string();
|
|
req.has_role_path = true;
|
|
req.role_path = "m/44'/1237'/0'/0/0".to_string();
|
|
assert!(selector_resolve(&req, &table).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_variable_path_in_range() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role = true;
|
|
req.role_name = "nostr_range".to_string();
|
|
req.has_role_path = true;
|
|
req.role_path = "m/44'/1237'/2'/0/0".to_string();
|
|
assert!(selector_resolve(&req, &table).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_variable_path_out_of_range() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role = true;
|
|
req.role_name = "nostr_range".to_string();
|
|
req.has_role_path = true;
|
|
req.role_path = "m/44'/1237'/5'/0/0".to_string();
|
|
assert_eq!(selector_resolve(&req, &table), Err(SelectorError::PathMismatch));
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_without_path() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role = true;
|
|
req.role_name = "nostr_range".to_string();
|
|
// Variable path role without role_path → PathRequired
|
|
assert_eq!(selector_resolve(&req, &table), Err(SelectorError::PathRequired));
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_without_role() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role_path = true;
|
|
req.role_path = "m/44'/1237'/0'/0/0".to_string();
|
|
assert_eq!(selector_resolve(&req, &table), Err(SelectorError::RoleRequired));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_role() {
|
|
let table = make_test_table();
|
|
let mut req = SelectorRequest::new();
|
|
req.has_role = true;
|
|
req.role_name = "nonexistent".to_string();
|
|
req.has_role_path = true;
|
|
req.role_path = "m/44'/1237'/0'/0/0".to_string();
|
|
assert_eq!(selector_resolve(&req, &table), Err(SelectorError::NotFound));
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_selector_with_default() {
|
|
let table = make_test_table();
|
|
let req = SelectorRequest::new();
|
|
// No selector — should find "main" as default
|
|
assert!(selector_resolve(&req, &table).is_ok());
|
|
}
|
|
}
|