diff --git a/.changes/shell-schema-required-sidcar.md b/.changes/shell-schema-required-sidcar.md new file mode 100644 index 000000000..7e48f6531 --- /dev/null +++ b/.changes/shell-schema-required-sidcar.md @@ -0,0 +1,6 @@ +--- +"shell": "patch" +--- + +Fix the plugin schema requiring to set `sidecar` property when it is in fact optional. + diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs index 5a641eb61..cb9d00daf 100644 --- a/plugins/fs/build.rs +++ b/plugins/fs/build.rs @@ -24,14 +24,16 @@ enum FsScopeEntry { }, } -// Ensure scope entry is kept up to date -impl From for scope::EntryRaw { - fn from(value: FsScopeEntry) -> Self { - match value { - FsScopeEntry::Value(path) => scope::EntryRaw::Value(path), - FsScopeEntry::Object { path } => scope::EntryRaw::Object { path }, - } - } +// Ensure `FsScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match scope::EntryRaw::Value(PathBuf::new()) { + scope::EntryRaw::Value(path) => FsScopeEntry::Value(path), + scope::EntryRaw::Object { path } => FsScopeEntry::Object { path }, + }; + match FsScopeEntry::Value(PathBuf::new()) { + FsScopeEntry::Value(path) => scope::EntryRaw::Value(path), + FsScopeEntry::Object { path } => scope::EntryRaw::Object { path }, + }; } const BASE_DIR_VARS: &[&str] = &[ diff --git a/plugins/fs/src/scope.rs b/plugins/fs/src/scope.rs index f31c786a6..a96c08b94 100644 --- a/plugins/fs/src/scope.rs +++ b/plugins/fs/src/scope.rs @@ -13,10 +13,9 @@ use std::{ use serde::Deserialize; -#[doc(hidden)] #[derive(Deserialize)] #[serde(untagged)] -pub enum EntryRaw { +pub(crate) enum EntryRaw { Value(PathBuf), Object { path: PathBuf }, } diff --git a/plugins/http/build.rs b/plugins/http/build.rs index 5f12208da..a4b802adf 100644 --- a/plugins/http/build.rs +++ b/plugins/http/build.rs @@ -47,23 +47,16 @@ enum HttpScopeEntry { }, } -// Ensure scope entry is kept up to date -impl From for scope::Entry { - fn from(value: HttpScopeEntry) -> Self { - let url = match value { - HttpScopeEntry::Value(url) => url, - HttpScopeEntry::Object { url } => url, - }; - - scope::Entry { - url: urlpattern::UrlPattern::parse( - urlpattern::UrlPatternInit::parse_constructor_string::(&url, None) - .unwrap(), - Default::default(), - ) - .unwrap(), - } - } +// Ensure `HttpScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match scope::EntryRaw::Value(String::new()) { + scope::EntryRaw::Value(url) => HttpScopeEntry::Value(url), + scope::EntryRaw::Object { url } => HttpScopeEntry::Object { url }, + }; + match HttpScopeEntry::Value(String::new()) { + HttpScopeEntry::Value(url) => scope::EntryRaw::Value(url), + HttpScopeEntry::Object { url } => scope::EntryRaw::Object { url }, + }; } fn main() { diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs index b84831deb..2123f215a 100644 --- a/plugins/http/src/scope.rs +++ b/plugins/http/src/scope.rs @@ -33,18 +33,18 @@ fn parse_url_pattern(s: &str) -> Result { UrlPattern::parse(init, Default::default()) } +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum EntryRaw { + Value(String), + Object { url: String }, +} + impl<'de> Deserialize<'de> for Entry { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { - #[derive(Deserialize)] - #[serde(untagged)] - enum EntryRaw { - Value(String), - Object { url: String }, - } - EntryRaw::deserialize(deserializer).and_then(|raw| { let url = match raw { EntryRaw::Value(url) => url, diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index d226fa4e3..4fb8528f2 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -20,7 +20,6 @@ serde = { workspace = true } [dependencies] serde = { workspace = true } -schemars = { workspace = true } serde_json = { workspace = true } tauri = { workspace = true } tokio = { version = "1", features = ["time"] } diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index fbfbb470b..34dbf87e6 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -2,15 +2,171 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::path::PathBuf; + +use schemars::JsonSchema; + #[path = "src/scope_entry.rs"] mod scope_entry; +/// A command argument allowed to be executed by the webview API. +#[derive(Debug, PartialEq, Eq, Clone, Hash, schemars::JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellScopeEntryAllowedArg { + /// A non-configurable argument that is passed to the command in the order it was specified. + Fixed(String), + + /// A variable that is set while calling the command from the webview API. + /// + Var { + /// [regex] validator to require passed values to conform to an expected input. + /// + /// This will require the argument value passed to this variable to match the `validator` regex + /// before it will be executed. + /// + /// The regex string is by default surrounded by `^...$` to match the full string. + /// For example the `https?://\w+` regex would be registered as `^https?://\w+$`. + /// + /// [regex]: + validator: String, + + /// Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime. + /// + /// This means the regex will not match on the entire string by default, which might + /// be exploited if your regex allow unexpected input to be considered valid. + /// When using this option, make sure your regex is correct. + #[serde(default)] + raw: bool, + }, +} + +/// A set of command arguments allowed to be executed by the webview API. +/// +/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all +/// arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to +/// be passed to the attached command configuration. +#[derive(Debug, PartialEq, Eq, Clone, Hash, JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellScopeEntryAllowedArgs { + /// Use a simple boolean to allow all or disable all arguments to this command configuration. + Flag(bool), + + /// A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration. + List(Vec), +} + +impl Default for ShellScopeEntryAllowedArgs { + fn default() -> Self { + Self::Flag(false) + } +} + +/// Shell scope entry. +#[derive(JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[allow(unused)] +pub(crate) enum ShellScopeEntry { + Command { + /// The name for this allowed shell command configuration. + /// + /// This name will be used inside of the webview API to call this command along with + /// any specified arguments. + name: String, + /// The command name. + /// It can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + // use default just so the schema doesn't flag it as required + #[serde(rename = "cmd")] + command: PathBuf, + /// The allowed arguments for the command execution. + args: ShellScopeEntryAllowedArgs, + }, + SideCar { + /// The name for this allowed shell command configuration. + /// + /// This name will be used inside of the webview API to call this command along with + /// any specified arguments. + name: String, + /// The allowed arguments for the command execution. + args: ShellScopeEntryAllowedArgs, + /// If this command is a sidecar command. + sidecar: bool, + }, +} + +// Ensure `ShellScopeEntry` and `scope_entry::EntryRaw` +// and `ShellScopeEntryAllowedArg` and `ShellAllowedArg` +// and `ShellScopeEntryAllowedArgs` and `ShellAllowedArgs` +// are kept in sync +#[allow(clippy::unnecessary_operation)] +fn _f() { + match (ShellScopeEntry::SideCar { + name: String::new(), + args: ShellScopeEntryAllowedArgs::Flag(false), + sidecar: true, + }) { + ShellScopeEntry::Command { + name, + command, + args, + } => scope_entry::EntryRaw { + name, + command: Some(command), + args: match args { + ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag), + ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List( + vec.into_iter() + .map(|s| match s { + ShellScopeEntryAllowedArg::Fixed(fixed) => { + scope_entry::ShellAllowedArg::Fixed(fixed) + } + ShellScopeEntryAllowedArg::Var { validator, raw } => { + scope_entry::ShellAllowedArg::Var { validator, raw } + } + }) + .collect(), + ), + }, + sidecar: false, + }, + ShellScopeEntry::SideCar { + name, + args, + sidecar, + } => scope_entry::EntryRaw { + name, + command: None, + args: match args { + ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag), + ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List( + vec.into_iter() + .map(|s| match s { + ShellScopeEntryAllowedArg::Fixed(fixed) => { + scope_entry::ShellAllowedArg::Fixed(fixed) + } + ShellScopeEntryAllowedArg::Var { validator, raw } => { + scope_entry::ShellAllowedArg::Var { validator, raw } + } + }) + .collect(), + ), + }, + sidecar, + }, + }; +} + const COMMANDS: &[&str] = &["execute", "spawn", "stdin_write", "kill", "open"]; fn main() { tauri_plugin::Builder::new(COMMANDS) .global_api_script_path("./api-iife.js") - .global_scope_schema(schemars::schema_for!(scope_entry::Entry)) + .global_scope_schema(schemars::schema_for!(ShellScopeEntry)) .android_path("android") .ios_path("ios") .build(); diff --git a/plugins/shell/src/scope_entry.rs b/plugins/shell/src/scope_entry.rs index a2fb61367..598391787 100644 --- a/plugins/shell/src/scope_entry.rs +++ b/plugins/shell/src/scope_entry.rs @@ -7,29 +7,23 @@ use serde::{de::Error as DeError, Deserialize, Deserializer}; use std::path::PathBuf; /// A command allowed to be executed by the webview API. -#[derive(Debug, Clone, PartialEq, Eq, Hash, schemars::JsonSchema)] -pub struct Entry { - /// The name for this allowed shell command configuration. - /// - /// This name will be used inside of the webview API to call this command along with - /// any specified arguments. - pub name: String, +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Entry { + pub(crate) name: String, + pub(crate) command: PathBuf, + pub(crate) args: ShellAllowedArgs, + pub(crate) sidecar: bool, +} - /// The command name. - /// It can start with a variable that resolves to a system base directory. - /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, - /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, - /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, - /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. - // use default just so the schema doesn't flag it as required +#[derive(Deserialize)] +pub(crate) struct EntryRaw { + pub(crate) name: String, #[serde(rename = "cmd")] - pub command: PathBuf, - - /// The allowed arguments for the command execution. - pub args: ShellAllowedArgs, - - /// If this command is a sidecar command. - pub sidecar: bool, + pub(crate) command: Option, + #[serde(default)] + pub(crate) args: ShellAllowedArgs, + #[serde(default)] + pub(crate) sidecar: bool, } impl<'de> Deserialize<'de> for Entry { @@ -37,18 +31,7 @@ impl<'de> Deserialize<'de> for Entry { where D: Deserializer<'de>, { - #[derive(Deserialize)] - struct InnerEntry { - name: String, - #[serde(rename = "cmd")] - command: Option, - #[serde(default)] - args: ShellAllowedArgs, - #[serde(default)] - sidecar: bool, - } - - let config = InnerEntry::deserialize(deserializer)?; + let config = EntryRaw::deserialize(deserializer)?; if !config.sidecar && config.command.is_none() { return Err(DeError::custom( @@ -65,19 +48,11 @@ impl<'de> Deserialize<'de> for Entry { } } -/// A set of command arguments allowed to be executed by the webview API. -/// -/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all -/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to -/// be passed to the attached command configuration. -#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)] #[serde(untagged, deny_unknown_fields)] #[non_exhaustive] pub enum ShellAllowedArgs { - /// Use a simple boolean to allow all or disable all arguments to this command configuration. Flag(bool), - - /// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration. List(Vec), } @@ -87,33 +62,13 @@ impl Default for ShellAllowedArgs { } } -/// A command argument allowed to be executed by the webview API. -#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)] #[serde(untagged, deny_unknown_fields)] #[non_exhaustive] pub enum ShellAllowedArg { - /// A non-configurable argument that is passed to the command in the order it was specified. Fixed(String), - - /// A variable that is set while calling the command from the webview API. - /// Var { - /// [regex] validator to require passed values to conform to an expected input. - /// - /// This will require the argument value passed to this variable to match the `validator` regex - /// before it will be executed. - /// - /// The regex string is by default surrounded by `^...$` to match the full string. - /// For example the `https?://\w+` regex would be registered as `^https?://\w+$`. - /// - /// [regex]: validator: String, - - /// Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime. - /// - /// This means the regex will not match on the entire string by default, which might - /// be exploited if your regex allow unexpected input to be considered valid. - /// When using this option, make sure your regex is correct. #[serde(default)] raw: bool, }, diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 985a4c4a5..230ab3763 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -221,7 +221,7 @@ fn update_app() { let updater_extension = if let Some(updater_zip_ext) = updater_zip_ext { format!("{bundle_updater_ext}.{updater_zip_ext}") } else { - format!("{bundle_updater_ext}") + bundle_updater_ext }; let signature_extension = format!("{updater_extension}.sig"); let signature_path = out_bundle_path.with_extension(signature_extension); diff --git a/plugins/updater/tests/updater-migration/tests/update.rs b/plugins/updater/tests/updater-migration/tests/update.rs index 3b5cb7796..56cb2d300 100644 --- a/plugins/updater/tests/updater-migration/tests/update.rs +++ b/plugins/updater/tests/updater-migration/tests/update.rs @@ -413,7 +413,7 @@ fn update_app() { ), 2 => ( v2_config.version, - Box::new(|| v2::bundle_paths(&root_dir, &v2_config.version)) + Box::new(|| v2::bundle_paths(&root_dir, v2_config.version)) as Box Vec<(BundleTarget, PathBuf)>>, "-v2", ),