diff --git a/Cargo.lock b/Cargo.lock index 91bcecdca..7ddbb2b41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -810,6 +810,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -3198,6 +3208,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -3859,6 +3880,7 @@ dependencies = [ "schemars", "semver", "serde", + "serde-untagged", "serde_json", "serde_with", "serialize-to-javascript", @@ -4222,6 +4244,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" + [[package]] name = "typenum" version = "1.17.0" diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index d31d71ad2..1694c0d20 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -42,6 +42,7 @@ infer = "0.15" dunce = "1" log = "0.4.21" cargo_metadata = { version = "0.18", optional = true } +serde-untagged = "0.1" [target."cfg(target_os = \"macos\")".dependencies] swift-rs = { version = "1.0.6", optional = true, features = [ "build" ] } diff --git a/core/tauri-utils/src/acl/capability.rs b/core/tauri-utils/src/acl/capability.rs index d1a170263..a84b9b017 100644 --- a/core/tauri-utils/src/acl/capability.rs +++ b/core/tauri-utils/src/acl/capability.rs @@ -7,15 +7,19 @@ use std::{path::Path, str::FromStr}; use crate::{acl::Identifier, platform::Target}; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{Error, IntoDeserializer}, + Deserialize, Deserializer, Serialize, +}; +use serde_untagged::UntaggedEnumVisitor; use super::Scopes; /// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] /// or an object that references a permission and extends its scope. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(untagged)] pub enum PermissionEntry { /// Reference a permission or permission set by identifier. PermissionRef(Identifier), @@ -42,6 +46,34 @@ impl PermissionEntry { } } +impl<'de> Deserialize<'de> for PermissionEntry { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct ExtendedPermissionStruct { + identifier: Identifier, + #[serde(default, flatten)] + scope: Scopes, + } + + UntaggedEnumVisitor::new() + .string(|string| { + let de = string.into_deserializer(); + Identifier::deserialize(de).map(Self::PermissionRef) + }) + .map(|map| { + let ext_perm = map.deserialize::()?; + Ok(Self::ExtendedPermission { + identifier: ext_perm.identifier, + scope: ext_perm.scope, + }) + }) + .deserialize(deserializer) + } +} + /// A grouping and boundary mechanism developers can use to isolate access to the IPC layer. /// /// It controls application windows fine grained access to the Tauri core, application, or plugin commands. @@ -204,9 +236,9 @@ pub struct CapabilityRemote { } /// Capability formats accepted in a capability file. -#[derive(Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[serde(untagged)] +#[cfg_attr(feature = "schema", schemars(untagged))] +#[cfg_attr(test, derive(Debug, PartialEq))] pub enum CapabilityFile { /// A single capability. Capability(Capability), @@ -234,6 +266,36 @@ impl CapabilityFile { } } +impl<'de> Deserialize<'de> for CapabilityFile { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .seq(|seq| seq.deserialize::>().map(Self::List)) + .map(|map| { + #[derive(Deserialize)] + struct CapabilityNamedList { + capabilities: Vec, + } + + let value: serde_json::Map = map.deserialize()?; + if value.contains_key("capabilities") { + serde_json::from_value::(value.into()) + .map(|named| Self::NamedList { + capabilities: named.capabilities, + }) + .map_err(|e| serde_untagged::de::Error::custom(e.to_string())) + } else { + serde_json::from_value::(value.into()) + .map(Self::Capability) + .map_err(|e| serde_untagged::de::Error::custom(e.to_string())) + } + }) + .deserialize(deserializer) + } +} + impl FromStr for CapabilityFile { type Err = super::Error; @@ -309,3 +371,71 @@ mod build { } } } + +#[cfg(test)] +mod tests { + use crate::acl::{Identifier, Scopes}; + + use super::{Capability, CapabilityFile, PermissionEntry}; + + #[test] + fn permission_entry_de() { + let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap(); + let identifier_json = serde_json::to_string(&identifier).unwrap(); + assert_eq!( + serde_json::from_str::(&identifier_json).unwrap(), + PermissionEntry::PermissionRef(identifier.clone()) + ); + + assert_eq!( + serde_json::from_value::(serde_json::json!({ + "identifier": identifier, + "allow": [], + "deny": null + })) + .unwrap(), + PermissionEntry::ExtendedPermission { + identifier, + scope: Scopes { + allow: Some(vec![]), + deny: None + } + } + ); + } + + #[test] + fn capability_file_de() { + let capability = Capability { + identifier: "test".into(), + description: "".into(), + remote: None, + local: true, + windows: vec![], + webviews: vec![], + permissions: vec![], + platforms: None, + }; + let capability_json = serde_json::to_string(&capability).unwrap(); + + assert_eq!( + serde_json::from_str::(&capability_json).unwrap(), + CapabilityFile::Capability(capability.clone()) + ); + + assert_eq!( + serde_json::from_str::(&format!("[{capability_json}]")).unwrap(), + CapabilityFile::List(vec![capability.clone()]) + ); + + assert_eq!( + serde_json::from_str::(&format!( + "{{ \"capabilities\": [{capability_json}] }}" + )) + .unwrap(), + CapabilityFile::NamedList { + capabilities: vec![capability.clone()] + } + ); + } +} diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 3c9edaaad..8e89e3a53 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -18,6 +18,7 @@ use serde::{ Deserialize, Serialize, Serializer, }; use serde_json::Value as JsonValue; +use serde_untagged::UntaggedEnumVisitor; use serde_with::skip_serializing_none; use url::Url; @@ -270,7 +271,14 @@ impl<'de> Deserialize<'de> for BundleTarget { match BundleTargetInner::deserialize(deserializer)? { BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All), - BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {t}"))), + BundleTargetInner::All(t) => Err(DeError::custom(format!( + "invalid bundle type {t}, expected one of `all`, {}", + BundleType::all() + .iter() + .map(|b| format!("`{b}`")) + .collect::>() + .join(", ") + ))), BundleTargetInner::List(l) => Ok(Self::List(l)), BundleTargetInner::One(t) => Ok(Self::One(t)), } @@ -1708,9 +1716,9 @@ pub struct SecurityConfig { } /// A capability entry which can be either an inlined capability or a reference to a capability defined on its own file. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", untagged)] +#[serde(untagged)] pub enum CapabilityEntry { /// An inlined capability. Inlined(Capability), @@ -1718,6 +1726,18 @@ pub enum CapabilityEntry { Reference(String), } +impl<'de> Deserialize<'de> for CapabilityEntry { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .string(|string| Ok(Self::Reference(string.to_owned()))) + .map(|map| map.deserialize::().map(Self::Inlined)) + .deserialize(deserializer) + } +} + /// The application pattern. #[skip_serializing_none] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index 31322775b..e125e87ac 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -4255,6 +4255,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -5012,6 +5023,7 @@ dependencies = [ "schemars", "semver", "serde", + "serde-untagged", "serde_json", "serde_with", "serialize-to-javascript", @@ -5423,6 +5435,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "typeid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" + [[package]] name = "typenum" version = "1.17.0"