use crate::{
crypto, utils,
v1::{files, fs, optional_uuid_from_empty_string, response_payload, FileLocation, FileProperties},
};
use secstr::{SecUtf8, SecVec};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;
use snafu::{Backtrace, ResultExt, Snafu};
use std::{fmt, num::ParseIntError, str::FromStr};
use strum::{Display, EnumString};
use uuid::Uuid;
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Caller provided invalid argument: {}", message))]
BadArgument { message: String, backtrace: Backtrace },
#[snafu(display("Expected metadata to be base64-encoded, but cannot decode it as such"))]
CannotDecodeBase64Metadata {
metadata: String,
source: base64::DecodeError,
},
#[snafu(display(
"Expected \"base\" or hyphenated lowercased UUID, got unknown string of length: {}",
string_length
))]
CannotParseParentOrBaseFromString { string_length: usize, backtrace: Backtrace },
#[snafu(display(
"Expected \"none\" or hyphenated lowercased UUID, got unknown string of length: {}",
string_length
))]
CannotParseParentOrNoneFromString { string_length: usize, backtrace: Backtrace },
#[snafu(display("Failed to decrypt link key '{}': {}", metadata, source))]
DecryptLinkKeyFailed { metadata: String, source: crypto::Error },
#[snafu(display("Failed to decrypt location name {}: {}", metadata, source))]
DecryptLocationNameFailed { metadata: String, source: crypto::Error },
#[snafu(display("Failed to deserialize location name: {}", source))]
DeserializeLocationNameFailed { source: serde_json::Error },
#[snafu(display("Expire duration value '{}' is too short to be valid", value))]
DurationIsTooShort { value: String, backtrace: Backtrace },
#[snafu(display("Expire duration unit '{}' is unsupported", unit))]
DurationUnitUnsupported { unit: String, backtrace: Backtrace },
#[snafu(display("Expire duration value '{}' is not a number: {}", value, source))]
DurationValueIsNotNum { value: String, source: ParseIntError },
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Expire {
Never,
Hours(u32),
Days(u32),
}
utils::display_from_json!(Expire);
impl FromStr for Expire {
type Err = Error;
fn from_str(never_or_duration: &str) -> Result<Self, Self::Err> {
if never_or_duration.eq_ignore_ascii_case("never") {
Ok(Self::Never)
} else if never_or_duration.len() < 2 {
DurationIsTooShortSnafu {
value: never_or_duration.to_owned(),
}
.fail()
} else {
let (raw_value, unit) = never_or_duration.split_at(never_or_duration.len() - 1);
let value = str::parse::<u32>(raw_value).context(DurationValueIsNotNumSnafu {
value: never_or_duration,
})?;
match unit {
"d" => Ok(Self::Days(value)),
"h" => Ok(Self::Hours(value)),
other => DurationUnitUnsupportedSnafu { unit: other.to_owned() }.fail(),
}
}
}
}
impl<'de> Deserialize<'de> for Expire {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let never_or_duration_repr = String::deserialize(deserializer)?;
str::parse::<Self>(&never_or_duration_repr).map_err(|_err| {
de::Error::invalid_value(
de::Unexpected::Str(&never_or_duration_repr),
&"\"never\" or duration with time units, e.g. \"6h\" or \"1d\"",
)
})
}
}
impl Serialize for Expire {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
Expire::Never => serializer.serialize_str("never"),
Expire::Hours(hours) => serializer.serialize_str(&format!("{}h", hours)),
Expire::Days(days) => serializer.serialize_str(&format!("{}d", days)),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum ItemKind {
File,
Folder,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct FileStorageInfo {
pub bucket: String,
pub region: String,
pub chunks: u32,
}
impl fmt::Display for FileStorageInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{} [{} chunks]", self.region, self.bucket, self.chunks)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct FolderData {
pub uuid: Uuid,
#[serde(rename = "name")]
pub name_metadata: String,
pub parent: ParentOrBase,
}
utils::display_from_json!(FolderData);
impl HasLocationName for FolderData {
fn name_metadata_ref(&self) -> &str {
&self.name_metadata
}
}
impl HasUuid for FolderData {
fn uuid_ref(&self) -> &Uuid {
&self.uuid
}
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LocationColor {
Default,
Blue,
Gray,
Green,
Purple,
Red,
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize, Ord, PartialOrd)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LocationKind {
Folder,
Sync,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct LocationNameMetadata {
pub name: String,
}
impl LocationNameMetadata {
#[allow(clippy::missing_panics_doc)]
pub fn encrypt_name_to_metadata<S: Into<String>>(name: S, key: &SecUtf8) -> String {
let name_json = json!(Self { name: name.into() }).to_string();
crypto::encrypt_metadata_str(&name_json, key, super::METADATA_VERSION).unwrap()
}
pub fn decrypt_name_from_metadata(name_metadata: &str, keys: &[SecUtf8]) -> Result<String> {
if name_metadata.eq_ignore_ascii_case("default") {
return Ok("Default".to_owned());
}
let decrypted_name_result =
crypto::decrypt_metadata_str_any_key(name_metadata, keys).context(DecryptLocationNameFailedSnafu {
metadata: name_metadata.to_owned(),
});
decrypted_name_result
.and_then(|name_metadata| Self::extract_name_from_folder_properties_json(name_metadata.as_bytes()))
}
pub fn decrypt_name_from_metadata_rsa(name_metadata: &str, rsa_private_key_bytes: &SecVec<u8>) -> Result<String> {
if name_metadata.eq_ignore_ascii_case("default") {
return Ok("Default".to_owned());
}
let decoded = base64::decode(name_metadata).context(CannotDecodeBase64MetadataSnafu {
metadata: name_metadata.to_owned(),
})?;
let decrypted_folder_properties_json = crypto::decrypt_rsa(&decoded, rsa_private_key_bytes.unsecure())
.context(DecryptLocationNameFailedSnafu {
metadata: name_metadata.to_owned(),
})?;
Self::extract_name_from_folder_properties_json(&decrypted_folder_properties_json)
}
pub fn encrypt_name_to_metadata_rsa<S: Into<String>>(
name: S,
rsa_public_key_bytes: &[u8],
) -> Result<String, crypto::Error> {
let name_json = json!(Self { name: name.into() }).to_string();
crypto::encrypt_rsa(name_json.as_bytes(), rsa_public_key_bytes).map(base64::encode)
}
#[must_use]
pub fn name_hashed(name: &str) -> String {
crypto::hash_fn(&name.to_lowercase())
}
pub(crate) fn extract_name_from_folder_properties_json(folder_properties_json_bytes: &[u8]) -> Result<String> {
serde_json::from_slice::<Self>(folder_properties_json_bytes)
.context(DeserializeLocationNameFailedSnafu {})
.map(|typed| typed.name)
}
}
pub trait HasFileMetadata {
fn file_metadata_ref(&self) -> &str;
fn decrypt_file_metadata(&self, master_keys: &[SecUtf8]) -> Result<FileProperties, files::Error> {
FileProperties::decrypt_file_metadata(self.file_metadata_ref(), master_keys)
}
}
pub trait HasSharedFileMetadata {
fn file_metadata_ref(&self) -> &str;
fn decrypt_file_metadata(&self, rsa_private_key_bytes: &SecVec<u8>) -> Result<FileProperties, files::Error> {
FileProperties::decrypt_file_metadata_rsa(self.file_metadata_ref(), rsa_private_key_bytes)
}
}
pub trait HasLinkedFileMetadata {
fn file_metadata_ref(&self) -> &str;
fn decrypt_file_metadata(&self, link_key: SecUtf8) -> Result<FileProperties, files::Error> {
FileProperties::decrypt_file_metadata(self.file_metadata_ref(), &[link_key])
}
}
pub trait HasFileLocation: HasUuid {
fn file_storage_ref(&self) -> &FileStorageInfo;
fn get_file_location(&self) -> FileLocation {
let storage = self.file_storage_ref();
FileLocation::new(&storage.region, &storage.bucket, *self.uuid_ref(), storage.chunks)
}
}
pub trait HasFiles<T: HasUuid + HasFileMetadata> {
fn files_ref(&self) -> &[T];
fn file_with_uuid(&self, uuid: &Uuid) -> Option<&T> {
self.files_ref().iter().find(|file_ref| file_ref.uuid_ref() == uuid)
}
fn decrypt_all_file_properties(&self, keys: &[SecUtf8]) -> Result<Vec<(&T, FileProperties)>, files::Error> {
self.files_ref()
.iter()
.map(|data| data.decrypt_file_metadata(keys).map(|properties| (data, properties)))
.collect::<Result<Vec<_>, files::Error>>()
}
}
pub trait HasFolders<T: HasUuid + HasLocationName> {
fn folders_ref(&self) -> &[T];
fn folder_with_uuid(&self, uuid: &Uuid) -> Option<&T> {
self.folders_ref()
.iter()
.find(|folder_ref| folder_ref.uuid_ref() == uuid)
}
fn decrypt_all_folder_names(&self, keys: &[SecUtf8]) -> Result<Vec<(&T, String)>, fs::Error> {
self.folders_ref()
.iter()
.map(|data| data.decrypt_name_metadata(keys).map(|name| (data, name)))
.collect::<Result<Vec<_>, fs::Error>>()
}
}
pub trait HasLocationName {
fn name_metadata_ref(&self) -> &str;
fn decrypt_name_metadata(&self, master_keys: &[SecUtf8]) -> Result<String> {
LocationNameMetadata::decrypt_name_from_metadata(self.name_metadata_ref(), master_keys)
}
}
pub trait HasSharedLocationName {
fn name_metadata_ref(&self) -> &str;
fn decrypt_name_metadata(&self, rsa_private_key_bytes: &SecVec<u8>) -> Result<String> {
LocationNameMetadata::decrypt_name_from_metadata_rsa(self.name_metadata_ref(), rsa_private_key_bytes)
}
}
pub trait HasLinkedLocationName {
fn name_metadata_ref(&self) -> &str;
fn decrypt_name_metadata(&self, link_key: SecUtf8) -> Result<String> {
LocationNameMetadata::decrypt_name_from_metadata(self.name_metadata_ref(), &[link_key])
}
}
pub trait HasLinkKey {
fn link_key_metadata_ref(&self) -> Option<&str>;
fn decrypt_link_key(&self, master_keys: &[SecUtf8]) -> Result<SecUtf8> {
match self.link_key_metadata_ref() {
Some(link_key_metadata) => crypto::decrypt_metadata_str_any_key(link_key_metadata, master_keys)
.context(DecryptLinkKeyFailedSnafu {
metadata: link_key_metadata.to_owned(),
})
.map(SecUtf8::from),
None => BadArgumentSnafu {
message: "link key metadata is absent, cannot decrypt None",
}
.fail(),
}
}
}
pub trait HasUuid {
fn uuid_ref(&self) -> &Uuid;
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LocationTrashRequestPayload<'location_trash> {
#[serde(rename = "apiKey")]
pub api_key: &'location_trash SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('location_trash, LocationTrashRequestPayload);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LocationExistsRequestPayload<'location_exists> {
#[serde(rename = "apiKey")]
pub api_key: &'location_exists SecUtf8,
pub parent: ParentOrBase,
#[serde(rename = "nameHashed")]
pub name_hashed: String,
}
utils::display_from_json_with_lifetime!('location_exists, LocationExistsRequestPayload);
impl<'location_exists> LocationExistsRequestPayload<'location_exists> {
#[must_use]
pub fn new(api_key: &'location_exists SecUtf8, target_parent: ParentOrBase, target_name: &str) -> Self {
let name_hashed = LocationNameMetadata::name_hashed(target_name);
Self {
api_key,
parent: target_parent,
name_hashed,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct LocationExistsResponseData {
pub exists: bool,
#[serde(default)]
#[serde(deserialize_with = "optional_uuid_from_empty_string")]
pub uuid: Option<Uuid>,
}
utils::display_from_json!(LocationExistsResponseData);
response_payload!(
LocationExistsResponsePayload<LocationExistsResponseData>
);
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ParentOrBase {
Base,
Folder(Uuid),
}
utils::display_from_json!(ParentOrBase);
impl ParentOrBase {
#[inline]
#[must_use]
pub const fn as_parent_or_none(&self) -> ParentOrNone {
match *self {
Self::Base => ParentOrNone::None,
Self::Folder(id) => ParentOrNone::Folder(id),
}
}
}
impl FromStr for ParentOrBase {
type Err = Error;
fn from_str(base_or_id: &str) -> Result<Self, Self::Err> {
if base_or_id.eq_ignore_ascii_case("base") {
Ok(Self::Base)
} else {
match Uuid::parse_str(base_or_id) {
Ok(uuid) => Ok(Self::Folder(uuid)),
Err(_) => CannotParseParentOrBaseFromStringSnafu {
string_length: base_or_id.len(),
}
.fail(),
}
}
}
}
impl<'de> Deserialize<'de> for ParentOrBase {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let base_or_id = String::deserialize(deserializer)?;
if base_or_id.eq_ignore_ascii_case("base") {
Ok(Self::Base)
} else {
match Uuid::parse_str(&base_or_id) {
Ok(uuid) => Ok(Self::Folder(uuid)),
Err(_) => Err(de::Error::invalid_value(
de::Unexpected::Str(&base_or_id),
&"\"base\" or hyphenated lowercased UUID",
)),
}
}
}
}
impl Serialize for ParentOrBase {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
Self::Base => serializer.serialize_str("base"),
Self::Folder(uuid) => serializer.serialize_str(&uuid.as_hyphenated().to_string()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ParentOrNone {
None,
Folder(Uuid),
}
utils::display_from_json!(ParentOrNone);
impl ParentOrNone {
#[inline]
#[must_use]
pub const fn as_parent_or_base(&self) -> ParentOrBase {
match *self {
Self::None => ParentOrBase::Base,
Self::Folder(id) => ParentOrBase::Folder(id),
}
}
}
impl FromStr for ParentOrNone {
type Err = Error;
fn from_str(none_or_id: &str) -> Result<Self, Self::Err> {
if none_or_id.eq_ignore_ascii_case("none") {
Ok(Self::None)
} else {
match Uuid::parse_str(none_or_id) {
Ok(uuid) => Ok(Self::Folder(uuid)),
Err(_) => CannotParseParentOrNoneFromStringSnafu {
string_length: none_or_id.len(),
}
.fail(),
}
}
}
}
impl<'de> Deserialize<'de> for ParentOrNone {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let none_or_id = String::deserialize(deserializer)?;
if none_or_id.eq_ignore_ascii_case("none") {
Ok(Self::None)
} else {
match Uuid::parse_str(&none_or_id) {
Ok(uuid) => Ok(Self::Folder(uuid)),
Err(_) => Err(de::Error::invalid_value(
de::Unexpected::Str(&none_or_id),
&"\"none\" or hyphenated lowercased UUID",
)),
}
}
}
}
impl Serialize for ParentOrNone {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
ParentOrNone::None => serializer.serialize_str("none"),
ParentOrNone::Folder(uuid) => serializer.serialize_str(&uuid.as_hyphenated().to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn location_should_be_deserialized_from_empty_string_uuid() {
let json = r#"{"exists":false, "uuid":""}"#;
let result = serde_json::from_str::<LocationExistsResponseData>(json);
assert!(result.unwrap().uuid.is_none());
}
#[test]
fn expire_time_should_be_deserialized_from_hours() {
let json = r#""6h""#;
let expected = Expire::Hours(6);
let result = serde_json::from_str::<Expire>(json);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn expire_time_should_be_deserialized_from_days() {
let json = r#""30d""#;
let expected = Expire::Days(30);
let result = serde_json::from_str::<Expire>(json);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn expire_time_should_be_deserialized_from_never() {
let json = r#""never""#;
let expected = Expire::Never;
let result = serde_json::from_str::<Expire>(json);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn parent_kind_should_be_deserialized_from_base() {
let json = r#""base""#;
let expected = ParentOrBase::Base;
let result = serde_json::from_str::<ParentOrBase>(json);
assert_eq!(result.unwrap(), expected);
}
#[test]
fn parent_kind_should_be_deserialized_from_id() {
let json = r#""00000000-0000-0000-0000-000000000000""#;
let expected = ParentOrBase::Folder(Uuid::nil());
let result = serde_json::from_str::<ParentOrBase>(json);
assert_eq!(result.unwrap(), expected);
}
}