From 6aeda7867520a73f18c54c75843f2a3e9574ac19 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 25 Sep 2024 04:08:21 +0300 Subject: [PATCH 1/3] fix(shell): fix schema requiring `sidecar` property even though it is optional --- .changes/shell-schema-required-sidcar.md | 6 + plugins/fs/build.rs | 18 +-- plugins/fs/src/scope.rs | 3 +- plugins/http/build.rs | 27 ++--- plugins/http/src/scope.rs | 14 +-- plugins/shell/Cargo.toml | 1 - plugins/shell/build.rs | 136 ++++++++++++++++++++++- plugins/shell/src/scope_entry.rs | 81 +++----------- 8 files changed, 187 insertions(+), 99 deletions(-) create mode 100644 .changes/shell-schema-required-sidcar.md 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..fda0d3e22 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -2,15 +2,149 @@ // 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)] +#[allow(unused)] +pub(crate) struct ShellScopeEntry { + /// 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: Option, + /// The allowed arguments for the command execution. + #[serde(default)] + args: ShellScopeEntryAllowedArgs, + /// If this command is a sidecar command. + #[serde(default)] + sidecar: bool, +} + +// Ensure `ShellScopeEntry` and `scope_entry::EntryRaw` +// and `ShellScopeEntryAllowedArg` and `ShellAllowedArg` +// and `ShellScopeEntryAllowedArgs` and `ShellAllowedArgs` +// are kept in sync +fn _f() { + let v = scope_entry::EntryRaw { + name: String::new(), + command: None, + args: scope_entry::ShellAllowedArgs::default(), + sidecar: false, + }; + + ShellScopeEntry { + name: v.name, + command: v.command, + args: match v.args { + scope_entry::ShellAllowedArgs::Flag(flag) => ShellScopeEntryAllowedArgs::Flag(flag), + scope_entry::ShellAllowedArgs::List(vec) => ShellScopeEntryAllowedArgs::List( + vec.into_iter() + .map(|s| match s { + scope_entry::ShellAllowedArg::Fixed(fixed) => { + ShellScopeEntryAllowedArg::Fixed(fixed) + } + scope_entry::ShellAllowedArg::Var { validator, raw } => { + ShellScopeEntryAllowedArg::Var { validator, raw } + } + }) + .collect(), + ), + }, + sidecar: v.sidecar, + }; + + match ShellScopeEntryAllowedArgs::Flag(false) { + 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(), + ), + }; +} + 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, }, From 4e89ec7543b103b23bbd02871bb7a71e3f52a728 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 25 Sep 2024 04:24:22 +0300 Subject: [PATCH 2/3] fix clippy --- plugins/shell/build.rs | 1 + plugins/updater/tests/app-updater/tests/update.rs | 2 +- plugins/updater/tests/updater-migration/tests/update.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index fda0d3e22..2719b2c60 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -93,6 +93,7 @@ pub(crate) struct ShellScopeEntry { // and `ShellScopeEntryAllowedArg` and `ShellAllowedArg` // and `ShellScopeEntryAllowedArgs` and `ShellAllowedArgs` // are kept in sync +#[allow(clippy::unnecessary_operation)] fn _f() { let v = scope_entry::EntryRaw { name: String::new(), 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", ), From dc410cd244e29d52e342c5a500256f3f4411e610 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 26 Sep 2024 22:26:07 +0300 Subject: [PATCH 3/3] make `cmd` and `sidecar` exclusive --- plugins/shell/build.rs | 143 +++++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 61 deletions(-) diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index 2719b2c60..34dbf87e6 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -65,28 +65,38 @@ impl Default for ShellScopeEntryAllowedArgs { /// Shell scope entry. #[derive(JsonSchema)] +#[serde(untagged, deny_unknown_fields)] #[allow(unused)] -pub(crate) struct ShellScopeEntry { - /// 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: Option, - /// The allowed arguments for the command execution. - #[serde(default)] - args: ShellScopeEntryAllowedArgs, - /// If this command is a sidecar command. - #[serde(default)] - sidecar: bool, +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` @@ -95,48 +105,59 @@ pub(crate) struct ShellScopeEntry { // are kept in sync #[allow(clippy::unnecessary_operation)] fn _f() { - let v = scope_entry::EntryRaw { + match (ShellScopeEntry::SideCar { name: String::new(), - command: None, - args: scope_entry::ShellAllowedArgs::default(), - sidecar: false, - }; - - ShellScopeEntry { - name: v.name, - command: v.command, - args: match v.args { - scope_entry::ShellAllowedArgs::Flag(flag) => ShellScopeEntryAllowedArgs::Flag(flag), - scope_entry::ShellAllowedArgs::List(vec) => ShellScopeEntryAllowedArgs::List( - vec.into_iter() - .map(|s| match s { - scope_entry::ShellAllowedArg::Fixed(fixed) => { - ShellScopeEntryAllowedArg::Fixed(fixed) - } - scope_entry::ShellAllowedArg::Var { validator, raw } => { - ShellScopeEntryAllowedArg::Var { validator, raw } - } - }) - .collect(), - ), + 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, }, - sidecar: v.sidecar, - }; - - match ShellScopeEntryAllowedArgs::Flag(false) { - 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(), - ), }; }