@@ -625,6 +625,69 @@ impl<T: ScopeObject> CommandScope<T> {
625625 }
626626}
627627
628+ impl < T : ScopeObjectMatch > CommandScope < T > {
629+ /// Ensure all deny scopes were not matched and any allow scopes were.
630+ ///
631+ /// This **WILL** return `true` if the allow scopes are empty and the deny
632+ /// scopes did not trigger. If you require at least one allow scope, then
633+ /// ensure the allow scopes are not empty before calling this method.
634+ ///
635+ /// ```
636+ /// # use tauri::ipc::CommandScope;
637+ /// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> {
638+ /// if scope.allows().is_empty() {
639+ /// return Err("you need to specify at least 1 allow scope!");
640+ /// }
641+ /// # Ok(())
642+ /// # }
643+ /// ```
644+ ///
645+ /// # Example
646+ ///
647+ /// ```
648+ /// # use serde::{Serialize, Deserialize};
649+ /// # use url::Url;
650+ /// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command};
651+ /// #
652+ /// #[derive(Debug, Clone, Serialize, Deserialize)]
653+ /// # pub struct Scope;
654+ /// #
655+ /// # impl ScopeObjectMatch for Scope {
656+ /// # type Input = str;
657+ /// #
658+ /// # fn matches(&self, input: &str) -> bool {
659+ /// # true
660+ /// # }
661+ /// # }
662+ /// #
663+ /// # fn do_work(_: String) -> Result<String, &'static str> {
664+ /// # Ok("Output".into())
665+ /// # }
666+ /// #
667+ /// #[command]
668+ /// fn my_command(scope: CommandScope<Scope>, input: String) -> Result<String, &'static str> {
669+ /// if scope.matches(&input) {
670+ /// do_work(input)
671+ /// } else {
672+ /// Err("Scope didn't match input")
673+ /// }
674+ /// }
675+ /// ```
676+ pub fn matches ( & self , input : & T :: Input ) -> bool {
677+ // first make sure the input doesn't match any existing deny scope
678+ if self . deny . iter ( ) . any ( |s| s. matches ( input) ) {
679+ return false ;
680+ }
681+
682+ // if there are allow scopes, ensure the input matches at least 1
683+ if self . allow . is_empty ( ) {
684+ true
685+ } else {
686+ self . allow . iter ( ) . any ( |s| s. matches ( input) )
687+ }
688+ }
689+ }
690+
628691impl < ' a , R : Runtime , T : ScopeObject > CommandArg < ' a , R > for CommandScope < T > {
629692 /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`].
630693 fn from_command ( command : CommandItem < ' a , R > ) -> Result < Self , InvokeError > {
@@ -729,6 +792,51 @@ impl<T: Send + Sync + Debug + DeserializeOwned + 'static> ScopeObject for T {
729792 }
730793}
731794
795+ /// A [`ScopeObject`] whose validation can be represented as a `bool`.
796+ ///
797+ /// # Example
798+ ///
799+ /// ```
800+ /// # use serde::{Deserialize, Serialize};
801+ /// # use tauri::{ipc::ScopeObjectMatch, Url};
802+ /// #
803+ /// #[derive(Debug, Clone, Serialize, Deserialize)]
804+ /// #[serde(rename_all = "camelCase")]
805+ /// pub enum Scope {
806+ /// Domain(Url),
807+ /// StartsWith(String),
808+ /// }
809+ ///
810+ /// impl ScopeObjectMatch for Scope {
811+ /// type Input = str;
812+ ///
813+ /// fn matches(&self, input: &str) -> bool {
814+ /// match self {
815+ /// Scope::Domain(url) => {
816+ /// let parsed: Url = match input.parse() {
817+ /// Ok(parsed) => parsed,
818+ /// Err(_) => return false,
819+ /// };
820+ ///
821+ /// let domain = parsed.domain();
822+ ///
823+ /// domain.is_some() && domain == url.domain()
824+ /// }
825+ /// Scope::StartsWith(start) => input.starts_with(start),
826+ /// }
827+ /// }
828+ /// }
829+ /// ```
830+ pub trait ScopeObjectMatch : ScopeObject {
831+ /// The type of input expected to validate against the scope.
832+ ///
833+ /// This will be borrowed, so if you want to match on a `&str` this type should be `str`.
834+ type Input : ?Sized ;
835+
836+ /// Check if the input matches against the scope.
837+ fn matches ( & self , input : & Self :: Input ) -> bool ;
838+ }
839+
732840impl ScopeManager {
733841 pub ( crate ) fn get_global_scope_typed < R : Runtime , T : ScopeObject > (
734842 & self ,
0 commit comments