feat: improve deserialization errors by using serde-untagged on enums (#9952)

* Use serde-untagged instead of derive trait for capabilities structs

* Update core/tauri-utils/Cargo.toml

* improve errors for other untagged enums

* clippy

* add tests, fix deserialization

* use schemars attribute instead

---------

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Simon Richard 2024-06-26 16:54:49 -04:00 committed by GitHub
parent 878198777e
commit 167b51a8de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 205 additions and 8 deletions

28
Cargo.lock generated
View File

@ -810,6 +810,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@ -3198,6 +3208,17 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.203" version = "1.0.203"
@ -3859,6 +3880,7 @@ dependencies = [
"schemars", "schemars",
"semver", "semver",
"serde", "serde",
"serde-untagged",
"serde_json", "serde_json",
"serde_with", "serde_with",
"serialize-to-javascript", "serialize-to-javascript",
@ -4222,6 +4244,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typeid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"

View File

@ -42,6 +42,7 @@ infer = "0.15"
dunce = "1" dunce = "1"
log = "0.4.21" log = "0.4.21"
cargo_metadata = { version = "0.18", optional = true } cargo_metadata = { version = "0.18", optional = true }
serde-untagged = "0.1"
[target."cfg(target_os = \"macos\")".dependencies] [target."cfg(target_os = \"macos\")".dependencies]
swift-rs = { version = "1.0.6", optional = true, features = [ "build" ] } swift-rs = { version = "1.0.6", optional = true, features = [ "build" ] }

View File

@ -7,15 +7,19 @@
use std::{path::Path, str::FromStr}; use std::{path::Path, str::FromStr};
use crate::{acl::Identifier, platform::Target}; 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; use super::Scopes;
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] /// 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. /// or an object that references a permission and extends its scope.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PermissionEntry { pub enum PermissionEntry {
/// Reference a permission or permission set by identifier. /// Reference a permission or permission set by identifier.
PermissionRef(Identifier), PermissionRef(Identifier),
@ -42,6 +46,34 @@ impl PermissionEntry {
} }
} }
impl<'de> Deserialize<'de> for PermissionEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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::<ExtendedPermissionStruct>()?;
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. /// 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. /// 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. /// Capability formats accepted in a capability file.
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)] #[cfg_attr(feature = "schema", schemars(untagged))]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum CapabilityFile { pub enum CapabilityFile {
/// A single capability. /// A single capability.
Capability(Capability), Capability(Capability),
@ -234,6 +266,36 @@ impl CapabilityFile {
} }
} }
impl<'de> Deserialize<'de> for CapabilityFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
.map(|map| {
#[derive(Deserialize)]
struct CapabilityNamedList {
capabilities: Vec<Capability>,
}
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
if value.contains_key("capabilities") {
serde_json::from_value::<CapabilityNamedList>(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::<Capability>(value.into())
.map(Self::Capability)
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
}
})
.deserialize(deserializer)
}
}
impl FromStr for CapabilityFile { impl FromStr for CapabilityFile {
type Err = super::Error; 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::<PermissionEntry>(&identifier_json).unwrap(),
PermissionEntry::PermissionRef(identifier.clone())
);
assert_eq!(
serde_json::from_value::<PermissionEntry>(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::<CapabilityFile>(&capability_json).unwrap(),
CapabilityFile::Capability(capability.clone())
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
CapabilityFile::List(vec![capability.clone()])
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!(
"{{ \"capabilities\": [{capability_json}] }}"
))
.unwrap(),
CapabilityFile::NamedList {
capabilities: vec![capability.clone()]
}
);
}
}

View File

@ -18,6 +18,7 @@ use serde::{
Deserialize, Serialize, Serializer, Deserialize, Serialize, Serializer,
}; };
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use serde_untagged::UntaggedEnumVisitor;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use url::Url; use url::Url;
@ -270,7 +271,14 @@ impl<'de> Deserialize<'de> for BundleTarget {
match BundleTargetInner::deserialize(deserializer)? { match BundleTargetInner::deserialize(deserializer)? {
BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All), 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::<Vec<_>>()
.join(", ")
))),
BundleTargetInner::List(l) => Ok(Self::List(l)), BundleTargetInner::List(l) => Ok(Self::List(l)),
BundleTargetInner::One(t) => Ok(Self::One(t)), 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. /// 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))] #[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", untagged)] #[serde(untagged)]
pub enum CapabilityEntry { pub enum CapabilityEntry {
/// An inlined capability. /// An inlined capability.
Inlined(Capability), Inlined(Capability),
@ -1718,6 +1726,18 @@ pub enum CapabilityEntry {
Reference(String), Reference(String),
} }
impl<'de> Deserialize<'de> for CapabilityEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.string(|string| Ok(Self::Reference(string.to_owned())))
.map(|map| map.deserialize::<Capability>().map(Self::Inlined))
.deserialize(deserializer)
}
}
/// The application pattern. /// The application pattern.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]

18
tooling/cli/Cargo.lock generated
View File

@ -4255,6 +4255,17 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde-value" name = "serde-value"
version = "0.7.0" version = "0.7.0"
@ -5012,6 +5023,7 @@ dependencies = [
"schemars", "schemars",
"semver", "semver",
"serde", "serde",
"serde-untagged",
"serde_json", "serde_json",
"serde_with", "serde_with",
"serialize-to-javascript", "serialize-to-javascript",
@ -5423,6 +5435,12 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "typeid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"