use crate::{
crypto, queries, utils,
v1::{
files, fs, response_payload, Expire, FileProperties, HasFileMetadata, HasLinkKey, HasLocationName, HasUuid,
ItemKind, Lazy, LocationNameMetadata, ParentOrBase, PasswordState, PlainResponsePayload,
},
FilenSettings,
};
use secstr::SecUtf8;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::skip_serializing_none;
use snafu::{ResultExt, Snafu};
use strum::{Display, EnumString};
use uuid::Uuid;
type Result<T, E = Error> = std::result::Result<T, E>;
pub static LINK_EMPTY_PASSWORD_VALUE: Lazy<String> = Lazy::new(|| PasswordState::Empty.to_string());
pub static LINK_EMPTY_PASSWORD_HASH: Lazy<String> = Lazy::new(|| crypto::hash_fn(LINK_EMPTY_PASSWORD_VALUE.clone()));
pub static SEC_LINK_EMPTY_PASSWORD_VALUE: Lazy<SecUtf8> =
Lazy::new(|| SecUtf8::from(LINK_EMPTY_PASSWORD_VALUE.as_str()));
const DIR_LINK_ADD_PATH: &str = "/v1/dir/link/add";
const DIR_LINK_EDIT_PATH: &str = "/v1/dir/link/edit";
const DIR_LINK_REMOVE_PATH: &str = "/v1/dir/link/remove";
const DIR_LINK_STATUS_PATH: &str = "/v1/dir/link/status";
#[allow(clippy::enum_variant_names)]
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Failed to decrypt link key metadata '{}': {}", metadata, source))]
DecryptLinkKeyMetadataFailed { metadata: String, source: crypto::Error },
#[snafu(display("{}", source))]
DecryptLocationNameFailed { source: fs::Error },
#[snafu(display("{}", source))]
DecryptFileMetadataFailed { source: files::Error },
#[snafu(display("{} query failed: {}", DIR_LINK_ADD_PATH, source))]
DirLinkAddQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", DIR_LINK_EDIT_PATH, source))]
DirLinkEditQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", DIR_LINK_REMOVE_PATH, source))]
DirLinkRemoveQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", DIR_LINK_STATUS_PATH, source))]
DirLinkStatusQueryFailed { source: queries::Error },
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum DownloadBtnState {
Disable,
Enable,
}
#[derive(Clone, Copy, Debug, Deserialize_repr, Display, EnumString, Eq, Hash, PartialEq, Serialize_repr)]
#[repr(u8)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum DownloadBtnStateByte {
Disable = 0,
Enable = 1,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DirLinkAddRequestPayload<'dir_link_add> {
#[serde(rename = "apiKey")]
pub api_key: &'dir_link_add SecUtf8,
#[serde(rename = "downloadBtn")]
pub download_btn: DownloadBtnState,
pub expiration: Expire,
#[serde(rename = "key")]
pub key_metadata: &'dir_link_add str,
#[serde(rename = "linkUUID")]
pub link_uuid: Uuid,
pub metadata: String,
pub parent: ParentOrBase,
pub password: PasswordState,
#[serde(rename = "passwordHashed")]
pub password_hashed: &'dir_link_add str,
#[serde(rename = "type")]
pub link_type: ItemKind,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('dir_link_add, DirLinkAddRequestPayload);
impl<'dir_link_add> DirLinkAddRequestPayload<'dir_link_add> {
pub fn from_file_data<T: HasFileMetadata + HasUuid>(
api_key: &'dir_link_add SecUtf8,
file_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'dir_link_add str,
master_keys: &[SecUtf8],
) -> Result<Self> {
let file_properties = file_data
.decrypt_file_metadata(master_keys)
.context(DecryptFileMetadataFailedSnafu {})?;
Self::from_file_properties(
api_key,
*file_data.uuid_ref(),
&file_properties,
parent,
link_uuid,
link_key_metadata,
master_keys,
)
}
pub fn from_file_properties(
api_key: &'dir_link_add SecUtf8,
file_uuid: Uuid,
file_properties: &FileProperties,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'dir_link_add str,
master_keys: &[SecUtf8],
) -> Result<Self> {
let link_key = SecUtf8::from(
crypto::decrypt_metadata_str_any_key(link_key_metadata, master_keys).context(
DecryptLinkKeyMetadataFailedSnafu {
metadata: link_key_metadata.to_owned(),
},
)?,
);
let metadata = file_properties.to_metadata_string(&link_key);
Ok(Self {
api_key,
download_btn: DownloadBtnState::Enable,
expiration: Expire::Never,
key_metadata: link_key_metadata,
link_uuid,
metadata,
parent,
password: PasswordState::Empty,
password_hashed: &LINK_EMPTY_PASSWORD_HASH,
link_type: ItemKind::File,
uuid: file_uuid,
})
}
pub fn from_folder_data<T: HasLocationName + HasUuid>(
api_key: &'dir_link_add SecUtf8,
folder_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'dir_link_add str,
master_keys: &[SecUtf8],
) -> Result<Self> {
let folder_name = folder_data
.decrypt_name_metadata(master_keys)
.context(DecryptLocationNameFailedSnafu {})?;
Self::from_folder_name(
api_key,
*folder_data.uuid_ref(),
&folder_name,
parent,
link_uuid,
link_key_metadata,
master_keys,
)
}
pub fn from_folder_name(
api_key: &'dir_link_add SecUtf8,
folder_uuid: Uuid,
folder_name: &str,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'dir_link_add str,
master_keys: &[SecUtf8],
) -> Result<Self> {
let link_key = SecUtf8::from(
crypto::decrypt_metadata_str_any_key(link_key_metadata, master_keys).context(
DecryptLinkKeyMetadataFailedSnafu {
metadata: link_key_metadata.to_owned(),
},
)?,
);
let metadata = LocationNameMetadata::encrypt_name_to_metadata(folder_name, &link_key);
Ok(Self {
api_key,
download_btn: DownloadBtnState::Enable,
expiration: Expire::Never,
key_metadata: link_key_metadata,
link_uuid,
metadata,
parent,
password: PasswordState::Empty,
password_hashed: &LINK_EMPTY_PASSWORD_HASH,
link_type: ItemKind::Folder,
uuid: folder_uuid,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DirLinkEditRequestPayload<'dir_link_edit> {
#[serde(rename = "apiKey")]
pub api_key: &'dir_link_edit SecUtf8,
#[serde(rename = "downloadBtn")]
pub download_btn: DownloadBtnState,
pub expiration: Expire,
pub password: PasswordState,
#[serde(rename = "passwordHashed")]
pub password_hashed: String,
pub salt: String,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('dir_link_edit, DirLinkEditRequestPayload);
impl<'dir_link_edit> DirLinkEditRequestPayload<'dir_link_edit> {
#[must_use]
pub fn new(
api_key: &'dir_link_edit SecUtf8,
download_btn: DownloadBtnState,
item_uuid: Uuid,
expiration: Expire,
link_plain_password: Option<&SecUtf8>,
) -> Self {
let (password_hashed, salt) = link_plain_password.map_or_else(
|| crypto::encrypt_to_link_password_and_salt(&SEC_LINK_EMPTY_PASSWORD_VALUE),
crypto::encrypt_to_link_password_and_salt,
);
Self {
api_key,
download_btn,
expiration,
password: link_plain_password.map_or(PasswordState::Empty, |_| PasswordState::NotEmpty),
password_hashed,
salt,
uuid: item_uuid,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DirLinkRemoveRequestPayload<'dir_link_remove> {
#[serde(rename = "apiKey")]
pub api_key: &'dir_link_remove SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('dir_link_remove, DirLinkRemoveRequestPayload);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DirLinkStatusRequestPayload<'dir_link_status> {
#[serde(rename = "apiKey")]
pub api_key: &'dir_link_status SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('dir_link_status, DirLinkStatusRequestPayload);
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct DirLinkStatusResponseData {
pub exists: bool,
pub uuid: Option<Uuid>,
pub key: Option<String>,
pub expiration: Option<u64>,
#[serde(rename = "expirationText")]
pub expiration_text: Option<Expire>,
#[serde(rename = "downloadBtn")]
pub download_btn: Option<DownloadBtnStateByte>,
pub password: Option<String>,
}
utils::display_from_json!(DirLinkStatusResponseData);
impl HasLinkKey for DirLinkStatusResponseData {
fn link_key_metadata_ref(&self) -> Option<&str> {
self.key.as_deref()
}
}
response_payload!(
DirLinkStatusResponsePayload<DirLinkStatusResponseData>
);
pub fn dir_link_add_request(
payload: &DirLinkAddRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(DIR_LINK_ADD_PATH, payload, filen_settings).context(DirLinkAddQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn dir_link_add_request_async(
payload: &DirLinkAddRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(DIR_LINK_ADD_PATH, payload, filen_settings)
.await
.context(DirLinkAddQueryFailedSnafu {})
}
pub fn dir_link_edit_request(
payload: &DirLinkEditRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(DIR_LINK_EDIT_PATH, payload, filen_settings).context(DirLinkEditQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn dir_link_edit_request_async(
payload: &DirLinkEditRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(DIR_LINK_EDIT_PATH, payload, filen_settings)
.await
.context(DirLinkEditQueryFailedSnafu {})
}
pub fn dir_link_remove_request(
payload: &DirLinkRemoveRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(DIR_LINK_REMOVE_PATH, payload, filen_settings).context(DirLinkRemoveQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn dir_link_remove_request_async(
payload: &DirLinkRemoveRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(DIR_LINK_REMOVE_PATH, payload, filen_settings)
.await
.context(DirLinkRemoveQueryFailedSnafu {})
}
pub fn dir_link_status_request(
payload: &DirLinkStatusRequestPayload,
filen_settings: &FilenSettings,
) -> Result<DirLinkStatusResponsePayload> {
queries::query_filen_api(DIR_LINK_STATUS_PATH, payload, filen_settings).context(DirLinkStatusQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn dir_link_status_request_async(
payload: &DirLinkStatusRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<DirLinkStatusResponsePayload> {
queries::query_filen_api_async(DIR_LINK_STATUS_PATH, payload, filen_settings)
.await
.context(DirLinkStatusQueryFailedSnafu {})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::validate_contract;
#[cfg(feature = "async")]
use crate::test_utils::validate_contract_async;
use once_cell::sync::Lazy;
use secstr::SecUtf8;
static API_KEY: Lazy<SecUtf8> =
Lazy::new(|| SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"));
#[test]
fn dir_link_status_request_should_have_proper_contract_for_no_link() {
let request_payload = DirLinkStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract(
DIR_LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/dir_link_status_no_link.json",
|request_payload, filen_settings| dir_link_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn dir_link_status_request_async_should_have_proper_contract_for_no_link() {
let request_payload = DirLinkStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract_async(
DIR_LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/dir_link_status_no_link.json",
|request_payload, filen_settings| async move {
dir_link_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn dir_link_status_request_should_have_proper_contract_for_link_without_password() {
let request_payload = DirLinkStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract(
DIR_LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/dir_link_status_no_password.json",
|request_payload, filen_settings| dir_link_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn dir_link_status_request_async_should_have_proper_contract_for_link_without_password() {
let request_payload = DirLinkStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract_async(
DIR_LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/dir_link_status_no_password.json",
|request_payload, filen_settings| async move {
dir_link_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
}