pub(crate) const DESTINATION: &str = "org.freedesktop.portal.Documents";
pub(crate) const PATH: &str = "/org/freedesktop/portal/documents";
use std::{
collections::HashMap,
ffi::CString,
fmt,
os::unix::{ffi::OsStrExt, prelude::AsRawFd},
path::{Path, PathBuf},
str::FromStr,
};
use enumflags2::{bitflags, BitFlags};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use zbus::zvariant::{Fd, OwnedValue, Type};
use crate::{
helpers::{call_method, path_from_null_terminated},
Error,
};
#[bitflags]
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Copy, Clone, Debug, Type)]
#[repr(u32)]
pub enum Flags {
ReuseExisting,
Persistent,
AsNeededByApp,
ExportDirectory,
}
pub type DocumentID<'a> = &'a str;
pub type OwnedDocumentID = String;
pub type ApplicationID<'a> = &'a str;
pub type OwnedApplicationID = String;
pub type Permissions = HashMap<OwnedApplicationID, Vec<Permission>>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Type)]
#[zvariant(signature = "s")]
#[serde(rename_all = "kebab-case")]
pub enum Permission {
Read,
Write,
GrantPermissions,
Delete,
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read => write!(f, "Read"),
Self::Write => write!(f, "Write"),
Self::GrantPermissions => write!(f, "Grant Permissions"),
Self::Delete => write!(f, "Delete"),
}
}
}
impl AsRef<str> for Permission {
fn as_ref(&self) -> &str {
match self {
Self::Read => "Read",
Self::Write => "Write",
Self::GrantPermissions => "Grant Permissions",
Self::Delete => "Delete",
}
}
}
impl From<Permission> for &'static str {
fn from(p: Permission) -> Self {
match p {
Permission::Read => "Read",
Permission::Write => "Write",
Permission::GrantPermissions => "Grant Permissions",
Permission::Delete => "Delete",
}
}
}
impl FromStr for Permission {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Read" | "read" => Ok(Permission::Read),
"Write" | "write" => Ok(Permission::Write),
"GrantPermissions" | "grant-permissions" => Ok(Permission::GrantPermissions),
"Delete" | "delete" => Ok(Permission::Delete),
_ => Err(Error::ParseError("Failed to parse priority, invalid value")),
}
}
}
#[derive(Debug)]
#[doc(alias = "org.freedesktop.portal.Documents")]
pub struct DocumentsProxy<'a>(zbus::Proxy<'a>);
impl<'a> DocumentsProxy<'a> {
pub async fn new(connection: &zbus::Connection) -> Result<DocumentsProxy<'a>, Error> {
let proxy = zbus::ProxyBuilder::new_bare(connection)
.interface("org.freedesktop.portal.Documents")?
.path(PATH)?
.destination(DESTINATION)?
.build()
.await?;
Ok(Self(proxy))
}
pub fn inner(&self) -> &zbus::Proxy<'_> {
&self.0
}
#[doc(alias = "Add")]
pub async fn add(
&self,
o_path_fd: &(impl AsRawFd + fmt::Debug),
reuse_existing: bool,
persistent: bool,
) -> Result<OwnedDocumentID, Error> {
call_method(
self.inner(),
"Add",
&(Fd::from(o_path_fd.as_raw_fd()), reuse_existing, persistent),
)
.await
}
#[doc(alias = "AddFull")]
pub async fn add_full(
&self,
o_path_fds: &[&impl AsRawFd],
flags: BitFlags<Flags>,
app_id: ApplicationID<'_>,
permissions: &[Permission],
) -> Result<(Vec<OwnedDocumentID>, HashMap<String, OwnedValue>), Error> {
let o_path: Vec<Fd> = o_path_fds.iter().map(|f| Fd::from(f.as_raw_fd())).collect();
call_method(
self.inner(),
"AddFull",
&(o_path, flags, app_id, permissions),
)
.await
}
#[doc(alias = "AddNamed")]
pub async fn add_named(
&self,
o_path_parent_fd: &(impl AsRawFd + fmt::Debug),
filename: impl AsRef<Path>,
reuse_existing: bool,
persistent: bool,
) -> Result<OwnedDocumentID, Error> {
let cstr = CString::new(filename.as_ref().as_os_str().as_bytes())
.expect("`filename` should not be null terminated");
call_method(
self.inner(),
"AddNamed",
&(
Fd::from(o_path_parent_fd.as_raw_fd()),
cstr.as_bytes_with_nul(),
reuse_existing,
persistent,
),
)
.await
}
#[doc(alias = "AddNamedFull")]
pub async fn add_named_full(
&self,
o_path_fd: &(impl AsRawFd + fmt::Debug),
filename: impl AsRef<Path>,
flags: BitFlags<Flags>,
app_id: ApplicationID<'_>,
permissions: &[Permission],
) -> Result<(OwnedDocumentID, HashMap<String, OwnedValue>), Error> {
let cstr = CString::new(filename.as_ref().as_os_str().as_bytes())
.expect("`filename` should not be null terminated");
call_method(
self.inner(),
"AddNamedFull",
&(
Fd::from(o_path_fd.as_raw_fd()),
cstr.as_bytes_with_nul(),
flags,
app_id,
permissions,
),
)
.await
}
#[doc(alias = "Delete")]
pub async fn delete(&self, doc_id: DocumentID<'_>) -> Result<(), Error> {
call_method(self.inner(), "Delete", &(doc_id)).await
}
#[doc(alias = "GetMountPoint")]
#[doc(alias = "get_mount_point")]
pub async fn mount_point(&self) -> Result<PathBuf, Error> {
let bytes: Vec<u8> = call_method(self.inner(), "GetMountPoint", &()).await?;
Ok(path_from_null_terminated(bytes))
}
#[doc(alias = "GrantPermissions")]
pub async fn grant_permissions(
&self,
doc_id: DocumentID<'_>,
app_id: ApplicationID<'_>,
permissions: &[Permission],
) -> Result<(), Error> {
call_method(
self.inner(),
"GrantPermissions",
&(doc_id, app_id, permissions),
)
.await
}
#[doc(alias = "Info")]
pub async fn info(&self, doc_id: DocumentID<'_>) -> Result<(PathBuf, Permissions), Error> {
let (bytes, permissions): (Vec<u8>, Permissions) =
call_method(self.inner(), "Info", &(doc_id)).await?;
Ok((path_from_null_terminated(bytes), permissions))
}
#[doc(alias = "List")]
pub async fn list(
&self,
app_id: ApplicationID<'_>,
) -> Result<HashMap<OwnedDocumentID, PathBuf>, Error> {
let response: HashMap<String, Vec<u8>> =
call_method(self.inner(), "List", &(app_id)).await?;
let mut new_response: HashMap<String, PathBuf> = HashMap::new();
for (key, bytes) in response {
new_response.insert(key, path_from_null_terminated(bytes));
}
Ok(new_response)
}
#[doc(alias = "Lookup")]
pub async fn lookup(
&self,
filename: impl AsRef<Path>,
) -> Result<Option<OwnedDocumentID>, Error> {
let cstr = CString::new(filename.as_ref().as_os_str().as_bytes())
.expect("`filename` should not be null terminated");
let doc_id: String =
call_method(self.inner(), "Lookup", &(cstr.as_bytes_with_nul())).await?;
if doc_id.is_empty() {
Ok(None)
} else {
Ok(Some(doc_id))
}
}
#[doc(alias = "RevokePermissions")]
pub async fn revoke_permissions(
&self,
doc_id: DocumentID<'_>,
app_id: ApplicationID<'_>,
permissions: &[Permission],
) -> Result<(), Error> {
call_method(
self.inner(),
"RevokePermissions",
&(doc_id, app_id, permissions),
)
.await
}
}
mod file_transfer;
pub use file_transfer::FileTransferProxy;
#[cfg(test)]
mod tests {
use crate::documents::Permission;
#[test]
fn serialize_deserialize() {
let permission = Permission::GrantPermissions;
let string = serde_json::to_string(&permission).unwrap();
assert_eq!(string, "\"grant-permissions\"");
let decoded = serde_json::from_str(&string).unwrap();
assert_eq!(permission, decoded);
}
}