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)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110pub struct Capability {
111 pub identifier: String,
118 #[serde(default)]
127 pub description: String,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub remote: Option<CapabilityRemote>,
147 #[serde(default = "default_capability_local")]
149 pub local: bool,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub windows: Vec<String>,
164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub webviews: Vec<String>,
175 #[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
195 pub permissions: Vec<PermissionEntry>,
196 #[serde(skip_serializing_if = "Option::is_none")]
204 pub platforms: Option<Vec<Target>>,
205}
206
207impl Capability {
208 pub fn is_active(&self, target: &Target) -> bool {
210 self
211 .platforms
212 .as_ref()
213 .map(|platforms| platforms.contains(target))
214 .unwrap_or(true)
215 }
216}
217
218#[cfg(feature = "schema")]
219fn unique_permission(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
220 use schemars::schema;
221 schema::SchemaObject {
222 instance_type: Some(schema::InstanceType::Array.into()),
223 array: Some(Box::new(schema::ArrayValidation {
224 unique_items: Some(true),
225 items: Some(gen.subschema_for::<PermissionEntry>().into()),
226 ..Default::default()
227 })),
228 ..Default::default()
229 }
230 .into()
231}
232
233fn default_capability_local() -> bool {
234 true
235}
236
237#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
240#[serde(rename_all = "camelCase")]
241pub struct CapabilityRemote {
242 pub urls: Vec<String>,
249}
250
251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[cfg_attr(feature = "schema", schemars(untagged))]
254#[cfg_attr(test, derive(Debug, PartialEq))]
255pub enum CapabilityFile {
256 Capability(Capability),
258 List(Vec<Capability>),
260 NamedList {
262 capabilities: Vec<Capability>,
264 },
265}
266
267impl CapabilityFile {
268 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
270 let path = path.as_ref();
271 let capability_file =
272 std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
273 let ext = path.extension().unwrap().to_string_lossy().to_string();
274 let file: Self = match ext.as_str() {
275 "toml" => toml::from_str(&capability_file)?,
276 "json" => serde_json::from_str(&capability_file)?,
277 #[cfg(feature = "config-json5")]
278 "json5" => json5::from_str(&capability_file)?,
279 _ => return Err(super::Error::UnknownCapabilityFormat(ext)),
280 };
281 Ok(file)
282 }
283}
284
285impl<'de> Deserialize<'de> for CapabilityFile {
286 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
287 where
288 D: Deserializer<'de>,
289 {
290 UntaggedEnumVisitor::new()
291 .seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
292 .map(|map| {
293 #[derive(Deserialize)]
294 struct CapabilityNamedList {
295 capabilities: Vec<Capability>,
296 }
297
298 let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
299 if value.contains_key("capabilities") {
300 serde_json::from_value::<CapabilityNamedList>(value.into())
301 .map(|named| Self::NamedList {
302 capabilities: named.capabilities,
303 })
304 .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
305 } else {
306 serde_json::from_value::<Capability>(value.into())
307 .map(Self::Capability)
308 .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
309 }
310 })
311 .deserialize(deserializer)
312 }
313}
314
315impl FromStr for CapabilityFile {
316 type Err = super::Error;
317
318 fn from_str(s: &str) -> Result<Self, Self::Err> {
319 serde_json::from_str(s)
320 .or_else(|_| toml::from_str(s))
321 .map_err(Into::into)
322 }
323}
324
325#[cfg(feature = "build")]
326mod build {
327 use std::convert::identity;
328
329 use proc_macro2::TokenStream;
330 use quote::{quote, ToTokens, TokenStreamExt};
331
332 use super::*;
333 use crate::{literal_struct, tokens::*};
334
335 impl ToTokens for CapabilityRemote {
336 fn to_tokens(&self, tokens: &mut TokenStream) {
337 let urls = vec_lit(&self.urls, str_lit);
338 literal_struct!(
339 tokens,
340 ::tauri::utils::acl::capability::CapabilityRemote,
341 urls
342 );
343 }
344 }
345
346 impl ToTokens for PermissionEntry {
347 fn to_tokens(&self, tokens: &mut TokenStream) {
348 let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
349
350 tokens.append_all(match self {
351 Self::PermissionRef(id) => {
352 quote! { #prefix::PermissionRef(#id) }
353 }
354 Self::ExtendedPermission { identifier, scope } => {
355 quote! { #prefix::ExtendedPermission {
356 identifier: #identifier,
357 scope: #scope
358 } }
359 }
360 });
361 }
362 }
363
364 impl ToTokens for Capability {
365 fn to_tokens(&self, tokens: &mut TokenStream) {
366 let identifier = str_lit(&self.identifier);
367 let description = str_lit(&self.description);
368 let remote = opt_lit(self.remote.as_ref());
369 let local = self.local;
370 let windows = vec_lit(&self.windows, str_lit);
371 let webviews = vec_lit(&self.webviews, str_lit);
372 let permissions = vec_lit(&self.permissions, identity);
373 let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
374
375 literal_struct!(
376 tokens,
377 ::tauri::utils::acl::capability::Capability,
378 identifier,
379 description,
380 remote,
381 local,
382 windows,
383 webviews,
384 permissions,
385 platforms
386 );
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use crate::acl::{Identifier, Scopes};
394
395 use super::{Capability, CapabilityFile, PermissionEntry};
396
397 #[test]
398 fn permission_entry_de() {
399 let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
400 let identifier_json = serde_json::to_string(&identifier).unwrap();
401 assert_eq!(
402 serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
403 PermissionEntry::PermissionRef(identifier.clone())
404 );
405
406 assert_eq!(
407 serde_json::from_value::<PermissionEntry>(serde_json::json!({
408 "identifier": identifier,
409 "allow": [],
410 "deny": null
411 }))
412 .unwrap(),
413 PermissionEntry::ExtendedPermission {
414 identifier,
415 scope: Scopes {
416 allow: Some(vec![]),
417 deny: None
418 }
419 }
420 );
421 }
422
423 #[test]
424 fn capability_file_de() {
425 let capability = Capability {
426 identifier: "test".into(),
427 description: "".into(),
428 remote: None,
429 local: true,
430 windows: vec![],
431 webviews: vec![],
432 permissions: vec![],
433 platforms: None,
434 };
435 let capability_json = serde_json::to_string(&capability).unwrap();
436
437 assert_eq!(
438 serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
439 CapabilityFile::Capability(capability.clone())
440 );
441
442 assert_eq!(
443 serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
444 CapabilityFile::List(vec![capability.clone()])
445 );
446
447 assert_eq!(
448 serde_json::from_str::<CapabilityFile>(&format!(
449 "{{ \"capabilities\": [{capability_json}] }}"
450 ))
451 .unwrap(),
452 CapabilityFile::NamedList {
453 capabilities: vec![capability.clone()]
454 }
455 );
456 }
457}