mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-11-28 12:27:16 +03:00
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:
parent
878198777e
commit
167b51a8de
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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" ] }
|
||||||
|
@ -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()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
18
tooling/cli/Cargo.lock
generated
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user