1use std::{path::Path, str::FromStr};
8
9use crate::{acl::Identifier, platform::Target};
10use serde::{
11 de::{Error, IntoDeserializer},
12 Deserialize, Deserializer, Serialize,
13};
14use serde_untagged::UntaggedEnumVisitor;
15
16use super::Scopes;
17
18#[derive(Debug, Clone, PartialEq, Serialize)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[serde(untagged)]
23pub enum PermissionEntry {
24 PermissionRef(Identifier),
26 ExtendedPermission {
28 identifier: Identifier,
30 #[serde(default, flatten)]
32 scope: Scopes,
33 },
34}
35
36impl PermissionEntry {
37 pub fn identifier(&self) -> &Identifier {
39 match self {
40 Self::PermissionRef(identifier) => identifier,
41 Self::ExtendedPermission {
42 identifier,
43 scope: _,
44 } => identifier,
45 }
46 }
47}
48
49impl<'de> Deserialize<'de> for PermissionEntry {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: Deserializer<'de>,
53 {
54 #[derive(Deserialize)]
55 struct ExtendedPermissionStruct {
56 identifier: Identifier,
57 #[serde(default, flatten)]
58 scope: Scopes,
59 }
60
61 UntaggedEnumVisitor::new()
62 .string(|string| {
63 let de = string.into_deserializer();
64 Identifier::deserialize(de).map(Self::PermissionRef)
65 })
66 .map(|map| {
67 let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
68 Ok(Self::ExtendedPermission {
69 identifier: ext_perm.identifier,
70 scope: ext_perm.scope,
71 })
72 })
73 .deserialize(deserializer)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
109pub struct Capability {
110 pub identifier: String,
117 #[serde(default)]
126 pub description: String,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub remote: Option<CapabilityRemote>,
146 #[serde(default = "default_capability_local")]
148 pub local: bool,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub windows: Vec<String>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub webviews: Vec<String>,
168 #[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
188 pub permissions: Vec<PermissionEntry>,
189 #[serde(skip_serializing_if = "Option::is_none")]
197 pub platforms: Option<Vec<Target>>,
198}
199
200impl Capability {
201 pub fn is_active(&self, target: &Target) -> bool {
203 self
204 .platforms
205 .as_ref()
206 .map(|platforms| platforms.contains(target))
207 .unwrap_or(true)
208 }
209}
210
211#[cfg(feature = "schema")]
212fn unique_permission(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
213 use schemars::schema;
214 schema::SchemaObject {
215 instance_type: Some(schema::InstanceType::Array.into()),
216 array: Some(Box::new(schema::ArrayValidation {
217 unique_items: Some(true),
218 items: Some(gen.subschema_for::<PermissionEntry>().into()),
219 ..Default::default()
220 })),
221 ..Default::default()
222 }
223 .into()
224}
225
226fn default_capability_local() -> bool {
227 true
228}
229
230#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
233#[serde(rename_all = "camelCase")]
234pub struct CapabilityRemote {
235 pub urls: Vec<String>,
242}
243
244#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246#[cfg_attr(feature = "schema", schemars(untagged))]
247#[cfg_attr(test, derive(Debug, PartialEq))]
248pub enum CapabilityFile {
249 Capability(Capability),
251 List(Vec<Capability>),
253 NamedList {
255 capabilities: Vec<Capability>,
257 },
258}
259
260impl CapabilityFile {
261 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
263 let path = path.as_ref();
264 let capability_file =
265 std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
266 let ext = path.extension().unwrap().to_string_lossy().to_string();
267 let file: Self = match ext.as_str() {
268 "toml" => toml::from_str(&capability_file)?,
269 "json" => serde_json::from_str(&capability_file)?,
270 #[cfg(feature = "config-json5")]
271 "json5" => json5::from_str(&capability_file)?,
272 _ => return Err(super::Error::UnknownCapabilityFormat(ext)),
273 };
274 Ok(file)
275 }
276}
277
278impl<'de> Deserialize<'de> for CapabilityFile {
279 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
280 where
281 D: Deserializer<'de>,
282 {
283 UntaggedEnumVisitor::new()
284 .seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
285 .map(|map| {
286 #[derive(Deserialize)]
287 struct CapabilityNamedList {
288 capabilities: Vec<Capability>,
289 }
290
291 let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
292 if value.contains_key("capabilities") {
293 serde_json::from_value::<CapabilityNamedList>(value.into())
294 .map(|named| Self::NamedList {
295 capabilities: named.capabilities,
296 })
297 .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
298 } else {
299 serde_json::from_value::<Capability>(value.into())
300 .map(Self::Capability)
301 .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
302 }
303 })
304 .deserialize(deserializer)
305 }
306}
307
308impl FromStr for CapabilityFile {
309 type Err = super::Error;
310
311 fn from_str(s: &str) -> Result<Self, Self::Err> {
312 serde_json::from_str(s)
313 .or_else(|_| toml::from_str(s))
314 .map_err(Into::into)
315 }
316}
317
318#[cfg(feature = "build")]
319mod build {
320 use std::convert::identity;
321
322 use proc_macro2::TokenStream;
323 use quote::{quote, ToTokens, TokenStreamExt};
324
325 use super::*;
326 use crate::{literal_struct, tokens::*};
327
328 impl ToTokens for CapabilityRemote {
329 fn to_tokens(&self, tokens: &mut TokenStream) {
330 let urls = vec_lit(&self.urls, str_lit);
331 literal_struct!(
332 tokens,
333 ::tauri::utils::acl::capability::CapabilityRemote,
334 urls
335 );
336 }
337 }
338
339 impl ToTokens for PermissionEntry {
340 fn to_tokens(&self, tokens: &mut TokenStream) {
341 let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
342
343 tokens.append_all(match self {
344 Self::PermissionRef(id) => {
345 quote! { #prefix::PermissionRef(#id) }
346 }
347 Self::ExtendedPermission { identifier, scope } => {
348 quote! { #prefix::ExtendedPermission {
349 identifier: #identifier,
350 scope: #scope
351 } }
352 }
353 });
354 }
355 }
356
357 impl ToTokens for Capability {
358 fn to_tokens(&self, tokens: &mut TokenStream) {
359 let identifier = str_lit(&self.identifier);
360 let description = str_lit(&self.description);
361 let remote = opt_lit(self.remote.as_ref());
362 let local = self.local;
363 let windows = vec_lit(&self.windows, str_lit);
364 let webviews = vec_lit(&self.webviews, str_lit);
365 let permissions = vec_lit(&self.permissions, identity);
366 let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
367
368 literal_struct!(
369 tokens,
370 ::tauri::utils::acl::capability::Capability,
371 identifier,
372 description,
373 remote,
374 local,
375 windows,
376 webviews,
377 permissions,
378 platforms
379 );
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use crate::acl::{Identifier, Scopes};
387
388 use super::{Capability, CapabilityFile, PermissionEntry};
389
390 #[test]
391 fn permission_entry_de() {
392 let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
393 let identifier_json = serde_json::to_string(&identifier).unwrap();
394 assert_eq!(
395 serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
396 PermissionEntry::PermissionRef(identifier.clone())
397 );
398
399 assert_eq!(
400 serde_json::from_value::<PermissionEntry>(serde_json::json!({
401 "identifier": identifier,
402 "allow": [],
403 "deny": null
404 }))
405 .unwrap(),
406 PermissionEntry::ExtendedPermission {
407 identifier,
408 scope: Scopes {
409 allow: Some(vec![]),
410 deny: None
411 }
412 }
413 );
414 }
415
416 #[test]
417 fn capability_file_de() {
418 let capability = Capability {
419 identifier: "test".into(),
420 description: "".into(),
421 remote: None,
422 local: true,
423 windows: vec![],
424 webviews: vec![],
425 permissions: vec![],
426 platforms: None,
427 };
428 let capability_json = serde_json::to_string(&capability).unwrap();
429
430 assert_eq!(
431 serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
432 CapabilityFile::Capability(capability.clone())
433 );
434
435 assert_eq!(
436 serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
437 CapabilityFile::List(vec![capability.clone()])
438 );
439
440 assert_eq!(
441 serde_json::from_str::<CapabilityFile>(&format!(
442 "{{ \"capabilities\": [{capability_json}] }}"
443 ))
444 .unwrap(),
445 CapabilityFile::NamedList {
446 capabilities: vec![capability.clone()]
447 }
448 );
449 }
450}