use crate::base::LKCError;
use chrono::{prelude::*, Duration, NaiveDateTime};
use hex;
use lazy_static::lazy_static;
use rand::prelude::*;
use regex::Regex;
use std::fmt;
lazy_static! {
#[doc(hidden)]
pub static ref RANDOMID_PATTERN: regex::Regex =
Regex::new(r"^[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}$")
.unwrap();
#[doc(hidden)]
pub static ref USERID_PATTERN: regex::Regex =
Regex::new(r"^([\p{L}\p{M}\p{N}\-_]|\.[^.])+$")
.unwrap();
#[doc(hidden)]
pub static ref CONTROL_CHARS_PATTERN: regex::Regex =
Regex::new(r#"\p{C}"#)
.unwrap();
#[doc(hidden)]
pub static ref DOMAIN_PATTERN: regex::Regex =
Regex::new(r"^([a-zA-Z0-9\-]+)(\.[a-zA-Z0-9\-]+)*$")
.unwrap();
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub enum IDType {
WorkspaceID,
UserID,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RandomID {
data: String,
}
impl RandomID {
pub fn generate() -> RandomID {
let mut rdata: [u8; 16] = [0; 16];
rand::thread_rng().fill_bytes(&mut rdata[..]);
let out = RandomID {
data: format!(
"{}-{}-{}-{}-{}",
hex::encode(&rdata[0..4]),
hex::encode(&rdata[4..6]),
hex::encode(&rdata[6..8]),
hex::encode(&rdata[8..10]),
hex::encode(&rdata[10..])
),
};
out
}
pub fn from(data: &str) -> Option<RandomID> {
if !RANDOMID_PATTERN.is_match(data) {
return None;
}
let mut out = RandomID {
data: String::from("00000000-0000-0000-0000-000000000000"),
};
out.data = data.to_lowercase();
Some(out)
}
pub fn from_userid(uid: &UserID) -> Option<RandomID> {
match uid.get_type() {
IDType::UserID => None,
IDType::WorkspaceID => Some(RandomID {
data: uid.to_string(),
}),
}
}
pub fn as_string(&self) -> &str {
&self.data
}
}
impl fmt::Display for RandomID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.data)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UserID {
data: String,
idtype: IDType,
}
impl UserID {
pub fn from(data: &str) -> Option<UserID> {
if data.len() > 64 || data.len() == 0 {
return None;
}
if !USERID_PATTERN.is_match(data) {
return None;
}
let mut out = UserID {
data: String::from(data),
idtype: IDType::UserID,
};
out.data = data.to_lowercase();
out.idtype = if RANDOMID_PATTERN.is_match(&out.data) {
IDType::WorkspaceID
} else {
IDType::UserID
};
Some(out)
}
pub fn from_wid(wid: &RandomID) -> UserID {
UserID {
data: String::from(wid.as_string()),
idtype: IDType::WorkspaceID,
}
}
pub fn as_string(&self) -> &str {
&self.data
}
pub fn get_type(&self) -> IDType {
self.idtype
}
}
impl fmt::Display for UserID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.data)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Domain {
data: String,
}
impl Domain {
pub fn from(data: &str) -> Option<Domain> {
if !DOMAIN_PATTERN.is_match(data) {
return None;
}
let mut out = Domain {
data: String::from(data),
};
out.data = data.to_lowercase();
Some(out)
}
pub fn as_string(&self) -> &str {
&self.data
}
pub fn parent(&self) -> Option<Domain> {
let domparts: Vec<&str> = self.data.split(".").collect();
if domparts.len() < 3 {
return None;
}
return Some(Domain {
data: String::from(&domparts[1..].join(".")),
});
}
pub fn pop(&mut self) -> Result<(), LKCError> {
let domparts: Vec<&str> = self.data.split(".").collect();
if domparts.len() < 3 {
return Err(LKCError::ErrOutOfRange);
}
self.data = String::from(&domparts[1..].join("."));
Ok(())
}
pub fn push(&mut self, subdom: &str) -> Result<(), LKCError> {
let mut domparts: Vec<&str> = self.data.split(".").collect();
let subdomparts: Vec<&str> = subdom.split(".").collect();
for part in subdomparts.iter().rev() {
domparts.insert(0, part);
}
self.data = String::from(&domparts.join("."));
Ok(())
}
}
impl fmt::Display for Domain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.data)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MAddress {
pub uid: UserID,
pub domain: Domain,
address: String,
}
impl MAddress {
pub fn from(data: &str) -> Option<MAddress> {
let parts = data.split("/").collect::<Vec<&str>>();
if parts.len() != 2 {
return None;
}
let out = MAddress {
uid: UserID::from(parts[0])?,
domain: Domain::from(parts[1])?,
address: format!("{}/{}", parts[0], parts[1]),
};
Some(out)
}
pub fn from_waddress(waddr: &WAddress) -> MAddress {
let uid = UserID::from_wid(&waddr.wid);
MAddress {
address: format!("{}/{}", uid, waddr.domain),
uid,
domain: waddr.domain.clone(),
}
}
pub fn from_parts(uid: &UserID, domain: &Domain) -> MAddress {
MAddress {
uid: uid.clone(),
domain: domain.clone(),
address: format!("{}/{}", uid, domain),
}
}
pub fn as_string(&self) -> String {
format!("{}/{}", self.uid, self.domain)
}
pub fn get_uid(&self) -> &UserID {
&self.uid
}
pub fn get_domain(&self) -> &Domain {
&self.domain
}
}
impl fmt::Display for MAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.uid, self.domain)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WAddress {
wid: RandomID,
domain: Domain,
address: String,
}
impl WAddress {
pub fn from(data: &str) -> Option<WAddress> {
let parts = data.split("/").collect::<Vec<&str>>();
if parts.len() != 2 {
return None;
}
let out = WAddress {
wid: RandomID::from(parts[0])?,
domain: Domain::from(parts[1])?,
address: format!("{}/{}", parts[0], parts[1]),
};
Some(out)
}
pub fn from_maddress(maddr: &MAddress) -> Option<WAddress> {
match maddr.uid.get_type() {
IDType::UserID => None,
IDType::WorkspaceID => Some(WAddress {
wid: RandomID::from_userid(&maddr.uid).unwrap(),
domain: maddr.domain.clone(),
address: format!("{}/{}", maddr.uid, maddr.domain),
}),
}
}
pub fn from_parts(wid: &RandomID, domain: &Domain) -> WAddress {
WAddress {
wid: wid.clone(),
domain: domain.clone(),
address: format!("{}/{}", wid, domain),
}
}
pub fn as_string(&self) -> String {
format!("{}/{}", self.wid, self.domain)
}
pub fn get_wid(&self) -> &RandomID {
&self.wid
}
pub fn get_domain(&self) -> &Domain {
&self.domain
}
}
impl fmt::Display for WAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.wid, self.domain)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArgonHash {
hash: String,
hashtype: String,
}
impl ArgonHash {
pub fn from(password: &str) -> ArgonHash {
ArgonHash {
hash: eznacl::hash_password(password, &eznacl::HashStrength::Basic),
hashtype: String::from("argon2id"),
}
}
pub fn from_hashstr(passhash: &str) -> ArgonHash {
ArgonHash {
hash: String::from(passhash),
hashtype: String::from("argon2id"),
}
}
pub fn get_hash(&self) -> &str {
&self.hash
}
pub fn get_hashtype(&self) -> &str {
&self.hashtype
}
}
impl fmt::Display for ArgonHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.hash)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Timestamp {
datetime: NaiveDateTime,
data: String,
}
impl Timestamp {
pub fn new() -> Timestamp {
let utc: DateTime<Utc> = Utc::now();
let formatted = utc.format("%Y%m%dT%H%M%SZ");
Timestamp::from_str(formatted.to_string().as_str()).unwrap()
}
pub fn from_str(data: &str) -> Option<Timestamp> {
let datetime = match chrono::NaiveDateTime::parse_from_str(data, "%Y%m%dT%H%M%SZ") {
Ok(v) => v,
Err(_) => return None,
};
Some(Timestamp {
datetime,
data: String::from(data.to_uppercase()),
})
}
pub fn from_datestr(date: &str) -> Option<Self> {
let datetime = format!("{}T000000Z", date);
Timestamp::from_str(&datetime)
}
pub fn with_offset(&self, days: i64) -> Option<Timestamp> {
let offset_date = self.datetime.checked_add_signed(Duration::days(days))?;
Some(Timestamp {
datetime: offset_date,
data: offset_date.format("%Y%m%dT%H%M%SZ").to_string(),
})
}
pub fn as_datestr(&self) -> &str {
&self.data[0..8]
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.data)
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn test_randomid() {
let testid = RandomID::generate();
let strid = RandomID::from(testid.as_string());
assert_ne!(strid, None);
}
#[test]
fn test_userid() {
assert_ne!(UserID::from("valid_e-mail.123"), None);
match UserID::from("11111111-1111-1111-1111-111111111111") {
Some(v) => {
assert!(v.get_type() == IDType::WorkspaceID)
}
None => {
panic!("test_userid failed workspace ID assignment")
}
}
match UserID::from("Valid.but.needs_case-squashed") {
Some(v) => {
assert_eq!(v.as_string(), "valid.but.needs_case-squashed")
}
None => {
panic!("test_userid failed case-squashing check")
}
}
assert_eq!(UserID::from("invalid..number1"), None);
assert_eq!(UserID::from("invalid#2"), None);
}
#[test]
fn test_domain() {
assert!(Domain::from("foo-bar").is_some());
assert!(Domain::from("foo-bar.baz.com").is_some());
match Domain::from("FOO.bar.com") {
Some(v) => {
assert_eq!(v.as_string(), "foo.bar.com")
}
None => {
panic!("test_domain failed case-squashing check")
}
}
assert!(Domain::from("a bad-id.com").is_none());
assert!(Domain::from("also_bad.org").is_none());
assert!(Domain::from("foo-bar.baz.com").unwrap().parent().is_some());
assert!(Domain::from("baz.com").unwrap().parent().is_none());
assert_eq!(
Domain::from("foo-bar.baz.com")
.unwrap()
.parent()
.unwrap()
.to_string(),
"baz.com"
);
let mut d = Domain::from("baz.com").unwrap();
assert!(d.push("foo.bar").is_ok());
assert_eq!(d.to_string(), "foo.bar.baz.com");
assert!(d.pop().is_ok());
assert_eq!(d.to_string(), "bar.baz.com");
assert!(d.pop().is_ok());
assert_eq!(d.to_string(), "baz.com");
assert!(d.pop().is_err());
}
#[test]
fn test_maddress() {
assert_ne!(MAddress::from("cats4life/example.com"), None);
assert_ne!(
MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com"),
None
);
let waddr = WAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com").unwrap();
assert_eq!(
MAddress::from_waddress(&waddr).to_string(),
"5a56260b-aa5c-4013-9217-a78f094432c3/example.com"
);
assert_eq!(MAddress::from("has spaces/example.com"), None);
assert_eq!(MAddress::from(r#"has_a_"/example.com"#), None);
assert_eq!(MAddress::from("\\not_allowed/example.com"), None);
assert_eq!(MAddress::from("/example.com"), None);
assert_eq!(
MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com/example.com"),
None
);
assert_eq!(MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3"), None);
}
#[test]
fn test_waddress() {
assert_ne!(
WAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com"),
None
);
assert_eq!(WAddress::from("cats4life/example.com"), None);
assert_eq!(
WAddress::from_maddress(&MAddress::from("cats4life/example.com").unwrap()),
None
);
assert!(WAddress::from_maddress(
&MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com").unwrap()
)
.is_some());
}
#[test]
fn test_timestamp() {
assert_eq!(Timestamp::from_str("foobar"), None);
let ts = Timestamp::from_str("20220501T131011Z").unwrap();
assert_eq!(&ts.to_string(), "20220501T131011Z");
assert_eq!(ts.as_datestr(), "20220501");
assert_eq!(&ts.with_offset(1).unwrap().to_string(), "20220502T131011Z");
let ds = Timestamp::from_datestr("20220502").unwrap();
assert_eq!(ds.as_datestr(), "20220502");
assert_eq!(&ds.to_string(), "20220502T000000Z");
}
}