diff --git a/contracts/validators/README.md b/contracts/validators/README.md index 3cceedde..9583b0f7 100644 --- a/contracts/validators/README.md +++ b/contracts/validators/README.md @@ -39,6 +39,12 @@ remain stable (guarded by a code-stability test). | `require_non_negative_amount` | `(i128) -> Result` | `AmountNegative` | | `checked_add_amount` | `(i128, i128) -> Result` | `Overflow` | | `require_in_range` | `(i128, i128, i128) -> Result` | `OutOfRange` | +| `capabilities` | `(&Env) -> u64` | — (read-only capability bitmap view) | + +The new `capabilities()` view returns a stable `u64` bitmask for the validator +features exposed by the current crate version. Clients can compare bitmasks +across upgrades to detect capability deltas without needing to inspect the +contract implementation directly. `MAX_VALIDATED_STRING_LEN` (256) is the maximum byte length accepted by the string validators. diff --git a/contracts/validators/src/lib.rs b/contracts/validators/src/lib.rs index 5382759e..7b605ad9 100644 --- a/contracts/validators/src/lib.rs +++ b/contracts/validators/src/lib.rs @@ -1,5 +1,19 @@ #![no_std] +mod errors; +mod validators; +mod views; + +pub use errors::ValidatorError; +pub use validators::{ + checked_add_amount, is_visible_ascii_metadata, normalize_visible_ascii, require_in_range, + require_non_negative_amount, require_positive_amount, MAX_VALIDATED_STRING_LEN, +}; +pub use views::{ + capabilities, ALL_CAPABILITIES, CAP_AMOUNT_VALIDATION, CAP_CHECKED_ARITHMETIC, + CAP_RANGE_VALIDATION, CAP_STRING_VALIDATION, +}; + //! Reusable, semantic input validators for the Callora contracts. //! //! This crate exists to replace generic panics and opaque `Result<_, ()>` @@ -14,80 +28,3 @@ //! - Acceptance iff `s` is non-empty, length ≤ [`MAX_VALIDATED_STRING_LEN`], //! every byte is in `0x20..=0x7E`, and neither the first nor last byte is //! ASCII space (`0x20`). - -use soroban_sdk::String; - -/// Error type for validation failures in [`normalize_visible_ascii`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidationError { - /// String is empty. - Empty, - /// String exceeds maximum length. - TooLong, - /// String contains non-visible ASCII characters. - InvalidCharacter, - /// String has leading or trailing spaces. - InvalidSpacing, -} - -/// Maximum byte length accepted by [`normalize_visible_ascii`]. -pub const MAX_VALIDATED_STRING_LEN: u32 = 256; - -/// Normalize a bounded metadata string to its canonical on-chain form. -/// -/// Accepted strings are non-empty visible ASCII with no leading or trailing -/// spaces. This rejects C0/DEL controls, zero-width and bidi controls, and -/// Unicode confusables by construction. Because ASCII has no decomposed forms, -/// the returned value is NFC-normalized and byte-stable. -/// -/// # Errors -/// Returns `Err(ValidationError)` when the string is empty, exceeds -/// [`MAX_VALIDATED_STRING_LEN`], contains a non-visible-ASCII byte, or has -/// leading/trailing ASCII space. -pub fn normalize_visible_ascii( - s: &String, -) -> Result<[u8; MAX_VALIDATED_STRING_LEN as usize], ValidationError> { - let len = s.len(); - if len == 0 { - return Err(ValidationError::Empty); - } - if len > MAX_VALIDATED_STRING_LEN { - return Err(ValidationError::TooLong); - } - - let mut buf = [0u8; MAX_VALIDATED_STRING_LEN as usize]; - s.copy_into_slice(&mut buf[..len as usize]); - let bytes = &buf[..len as usize]; - - if bytes[0] == b' ' || bytes[len as usize - 1] == b' ' { - return Err(ValidationError::InvalidSpacing); - } - - for &b in bytes { - if !(0x20..=0x7e).contains(&b) { - return Err(ValidationError::InvalidCharacter); - } - } - - Ok(buf) -} - -/// Return whether a bounded metadata string is accepted by the on-chain policy. -pub fn is_visible_ascii_metadata(s: &String) -> bool { - normalize_visible_ascii(s).is_ok() -} - -/// Pure reference predicate mirroring [`normalize_visible_ascii`] over raw bytes. -/// -/// Used by property tests to check the on-chain validator against an independent -/// oracle that does not touch the Soroban host. -pub fn bytes_are_visible_ascii(bytes: &[u8]) -> bool { - let len = bytes.len(); - if len == 0 || len > MAX_VALIDATED_STRING_LEN as usize { - return false; - } - if bytes[0] == b' ' || bytes[len - 1] == b' ' { - return false; - } - bytes.iter().all(|b| (0x20..=0x7e).contains(b)) -} diff --git a/contracts/validators/src/test_validators.rs b/contracts/validators/src/test_validators.rs index f730c586..709b65a3 100644 --- a/contracts/validators/src/test_validators.rs +++ b/contracts/validators/src/test_validators.rs @@ -4,7 +4,10 @@ use crate::validators::{ checked_add_amount, is_visible_ascii_metadata, normalize_visible_ascii, require_in_range, require_non_negative_amount, require_positive_amount, MAX_VALIDATED_STRING_LEN, }; -use crate::ValidatorError; +use crate::{ + capabilities, CAP_AMOUNT_VALIDATION, CAP_CHECKED_ARITHMETIC, CAP_RANGE_VALIDATION, + CAP_STRING_VALIDATION, ValidatorError, +}; use soroban_sdk::{Env, String}; // --- normalize_visible_ascii ------------------------------------------------- @@ -168,3 +171,21 @@ fn empty_range_rejects_everything() { // min > max is an empty range. assert_eq!(require_in_range(5, 10, 1), Err(ValidatorError::OutOfRange)); } + +// --- capability bitmap ------------------------------------------------------- + +#[test] +fn capabilities_exposes_supported_validator_features() { + let env = Env::default(); + let caps = capabilities(&env); + + assert_ne!(caps, 0); + assert_eq!( + caps, + CAP_STRING_VALIDATION + | CAP_AMOUNT_VALIDATION + | CAP_CHECKED_ARITHMETIC + | CAP_RANGE_VALIDATION + ); + assert_eq!(caps & !((1u64 << 4) - 1), 0); +} diff --git a/contracts/validators/src/views.rs b/contracts/validators/src/views.rs new file mode 100644 index 00000000..fb6b8989 --- /dev/null +++ b/contracts/validators/src/views.rs @@ -0,0 +1,53 @@ +use soroban_sdk::Env; + +/// Bit 0 — String validation: clients can rely on the visible-ASCII metadata +/// validators exposed by [`crate::normalize_visible_ascii`] and +/// [`crate::is_visible_ascii_metadata`]. +pub const CAP_STRING_VALIDATION: u64 = 1 << 0; + +/// Bit 1 — Amount validation: clients can use the positive/non-negative amount +/// preconditions exposed by [`crate::require_positive_amount`] and +/// [`crate::require_non_negative_amount`]. +pub const CAP_AMOUNT_VALIDATION: u64 = 1 << 1; + +/// Bit 2 — Checked arithmetic: clients can use the overflow-safe checked +/// addition helper [`crate::checked_add_amount`]. +pub const CAP_CHECKED_ARITHMETIC: u64 = 1 << 2; + +/// Bit 3 — Range validation: clients can use the inclusive range checks from +/// [`crate::require_in_range`]. +pub const CAP_RANGE_VALIDATION: u64 = 1 << 3; + +/// Bits 4–63 are reserved for future validator capabilities and remain clear. +pub const ALL_CAPABILITIES: u64 = CAP_STRING_VALIDATION + | CAP_AMOUNT_VALIDATION + | CAP_CHECKED_ARITHMETIC + | CAP_RANGE_VALIDATION; + +/// Return the validator capability bitmap. +/// +/// This is a pure read-only view and does not inspect or mutate storage. The +/// returned bitmask is stable for the current validator feature set so clients +/// can detect capability deltas across upgrades. +pub fn capabilities(_env: &Env) -> u64 { + ALL_CAPABILITIES +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::Env; + + #[test] + fn capabilities_equals_all_mask() { + let env = Env::default(); + assert_eq!(capabilities(&env), ALL_CAPABILITIES); + } + + #[test] + fn reserved_bits_are_clear() { + let env = Env::default(); + let caps = capabilities(&env); + assert_eq!(caps & !((1u64 << 4) - 1), 0); + } +}