aboutsummaryrefslogtreecommitdiff
path: root/rust/src/implementation
diff options
context:
space:
mode:
authorAndreas Grois <andi@grois.info>2022-10-10 21:30:02 +0200
committerAndreas Grois <andi@grois.info>2022-10-10 21:37:15 +0200
commite4ad766315879e1ff05bb111229f073f8f0ed68e (patch)
tree4b043ff47c78b2c00c80c94ebda622c32c8b6d3d /rust/src/implementation
PassFish: Initial Commit
Well, that's a lie. But nobody needs to see all the iterations I decided to sweep under the rug. That said, I think the repo is, while not clean, clean enough now, to not be embarrassed by uploading it to github.
Diffstat (limited to 'rust/src/implementation')
-rw-r--r--rust/src/implementation/mod.rs70
-rw-r--r--rust/src/implementation/passwordmaker/emittingsender.rs17
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs31
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs131
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/message_parsing.rs102
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/mod.rs49
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs46
-rw-r--r--rust/src/implementation/passwordmaker/mod.rs305
-rw-r--r--rust/src/implementation/passwordmaker/settings.rs159
-rw-r--r--rust/src/implementation/passwordmaker/thread_messages.rs41
-rw-r--r--rust/src/implementation/profiles.rs1127
-rw-r--r--rust/src/implementation/pwm_macros.rs27
12 files changed, 2105 insertions, 0 deletions
diff --git a/rust/src/implementation/mod.rs b/rust/src/implementation/mod.rs
new file mode 100644
index 0000000..889e743
--- /dev/null
+++ b/rust/src/implementation/mod.rs
@@ -0,0 +1,70 @@
+mod profiles;
+mod passwordmaker;
+mod pwm_macros;
+
+pub use self::profiles::Profiles;
+pub use self::passwordmaker::PasswordMaker;
+pub use self::passwordmaker::Settings;
+
+//------------------------------------------------------------------------------
+// helper types that are used in multiple modules.
+
+fn get_config_folder() -> Option<std::path::PathBuf> {
+ dirs::config_dir()
+ .map(|p| p.join("info.grois/harbour-passfish/"))
+}
+
+#[derive(Debug)]
+enum LoadError {
+ Xdg,
+ Loading(std::io::Error),
+ Parsing(toml::de::Error),
+}
+impl std::fmt::Display for LoadError {
+ fn fmt(&self, f : &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ LoadError::Xdg => { write!(f, "XDG config path not found") }
+ LoadError::Loading(e) => { e.fmt(f) }
+ LoadError::Parsing(e) => { e.fmt(f) }
+ }
+ }
+}
+impl std::error::Error for LoadError {}
+impl From<std::io::Error> for LoadError {
+ fn from(e : std::io::Error) -> Self {
+ LoadError::Loading(e)
+ }
+}
+impl From<toml::de::Error> for LoadError {
+ fn from(e : toml::de::Error) -> Self {
+ LoadError::Parsing(e)
+ }
+}
+
+
+#[derive(Debug)]
+enum StoreError {
+ Xdg,
+ Writing(std::io::Error),
+ Serialization(toml::ser::Error)
+}
+impl std::fmt::Display for StoreError {
+ fn fmt(&self, f : &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ StoreError::Xdg => { write!(f, "XDC config path not found") }
+ StoreError::Writing(e) => { e.fmt(f) }
+ StoreError::Serialization(e) => { e.fmt(f) }
+ }
+ }
+}
+impl std::error::Error for StoreError {}
+impl From<std::io::Error> for StoreError {
+ fn from(e: std::io::Error) -> Self {
+ StoreError::Writing(e)
+ }
+}
+impl From<toml::ser::Error> for StoreError {
+ fn from(e: toml::ser::Error) -> Self {
+ StoreError::Serialization(e)
+ }
+}
diff --git a/rust/src/implementation/passwordmaker/emittingsender.rs b/rust/src/implementation/passwordmaker/emittingsender.rs
new file mode 100644
index 0000000..1cf58fc
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/emittingsender.rs
@@ -0,0 +1,17 @@
+use std::sync::mpsc::{channel,Sender,Receiver,SendError};
+#[derive(Clone)]
+pub(crate) struct EmittingSender<MT, E : Fn()> {
+ sender : Sender<MT>,
+ emit : E,
+}
+
+impl <MT, E : Fn()> EmittingSender<MT, E>{
+ pub(crate) fn emitting_channel(emit : E) -> (Self, Receiver<MT>) {
+ let (sender,r) = channel();
+ (EmittingSender { sender, emit }, r)
+ }
+ pub(crate) fn send(&self, t: MT) -> Result<(), SendError<MT>> {
+ //first send, then notify.
+ self.sender.send(t).map(|_| (self.emit)())
+ }
+}
diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs
new file mode 100644
index 0000000..85fbc39
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs
@@ -0,0 +1,31 @@
+mod qt_hash;
+use passwordmaker_rs::{Hasher, HasherList};
+use ripemd::Digest;
+
+
+pub(super) use qt_hash::{QtMd4, QtMd5, QtSha1, QtSha256};
+pub(super) struct Ripemd160;
+
+impl Hasher for Ripemd160{
+ type Output = [u8;20];
+
+ fn hash(input : &[u8]) -> Self::Output {
+ let hash = ripemd::Ripemd160::digest(input);
+ hash.into()
+ }
+}
+
+pub(super) struct PassFishHashers;
+impl HasherList for PassFishHashers {
+ type MD4 = QtMd4;
+ type MD5 = QtMd5;
+ type SHA1 = QtSha1;
+ type SHA256 = QtSha256;
+ type RIPEMD160 = Ripemd160;
+}
+
+impl passwordmaker_rs::Md4 for QtMd4 {}
+impl passwordmaker_rs::Md5 for QtMd5 {}
+impl passwordmaker_rs::Sha1 for QtSha1 {}
+impl passwordmaker_rs::Sha256 for QtSha256 {}
+impl passwordmaker_rs::Ripemd160 for Ripemd160 {}
diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs
new file mode 100644
index 0000000..64d4e80
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs
@@ -0,0 +1,131 @@
+use std::borrow::{BorrowMut, Borrow};
+use passwordmaker_rs::Hasher;
+use libc::size_t;
+
+pub(crate) struct Md4;
+pub(crate) struct Md5;
+pub(crate) struct Sha1;
+pub(crate) struct Sha256;
+
+pub(crate) struct QHasher<T>(T);
+
+pub(crate) type QtMd4 = QHasher<Md4>;
+pub(crate) type QtMd5 = QHasher<Md5>;
+pub(crate) type QtSha1 = QHasher<Sha1>;
+pub(crate) type QtSha256 = QHasher<Sha256>;
+
+impl<T> Hasher for QHasher<T> where T:QtHasher {
+ type Output = T::Output;
+
+ fn hash(input : &[u8]) -> Self::Output {
+ let mut result = T::Output::default();
+ let required_bytes = result.borrow().len();
+ let computed_bytes = unsafe{
+ pwm_qhash(
+ T::QT_ALGO_NUMBER,
+ input.as_ptr(),
+ input.len() as size_t,
+ result.borrow_mut().as_mut_ptr(),
+ required_bytes)
+ };
+ assert_eq!(computed_bytes, required_bytes); //no point to forward this to caller. It's a code bug, plain and simple.
+ result
+ }
+}
+
+pub(crate) trait QtHasher{
+ type Output : Default + BorrowMut<[u8]>;
+ const QT_ALGO_NUMBER : size_t;
+}
+
+impl QtHasher for Md4 {
+ type Output = [u8;16];
+ const QT_ALGO_NUMBER : size_t = 0;
+}
+
+impl QtHasher for Md5 {
+ type Output = [u8;16];
+ const QT_ALGO_NUMBER : size_t = 1;
+}
+
+impl QtHasher for Sha1 {
+ type Output = [u8;20];
+ const QT_ALGO_NUMBER : size_t = 2;
+}
+
+impl QtHasher for Sha256 {
+ type Output = [u8;32];
+ const QT_ALGO_NUMBER : size_t = 4;
+}
+
+#[cfg(test)]
+use rust_testhelper::pwm_qhash;
+#[cfg(not(test))]
+extern "C"{
+ fn pwm_qhash(algorithm : size_t, input : *const u8, input_length : size_t, output : *mut u8, output_capacity : size_t) -> size_t;
+}
+
+/// Those tests are testing the integration of Qt's QCryptographicHash function. They are functional tests, NOT unit tests.
+#[cfg(test)]
+mod qt_hash_tests{
+ use super::*;
+ fn get_simple_string_as_bytes() -> &'static [u8] {
+ "I am a simple string and I like simple things. I like dancing in the rain, I like eating hamburgers, and I like you.".as_bytes()
+ }
+ fn get_complex_string_as_bytes() -> &'static [u8] {
+ "I am a complex string and do complex stuff. I like 🕺 in the 🌧️. I like eating 🍔, and I ❤️ you.".as_bytes()
+ }
+ #[test]
+ fn md4_simple_string_test(){
+ let hash = QtMd4::hash(get_simple_string_as_bytes());
+ let expected = vec![0x14, 0x1f, 0x1f, 0x1d, 0xef, 0x4a, 0x0d, 0x15, 0x1d, 0xb5, 0x5f, 0x7c, 0xb8, 0x96, 0xca, 0x99];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn md4_complex_string_test(){
+ let hash = QtMd4::hash(get_complex_string_as_bytes());
+ let expected = vec![0xc1, 0xfb, 0xe3, 0x4d, 0x72, 0x75, 0xb3, 0xa5, 0x3a, 0xc3, 0x45, 0xcf, 0x90, 0x34, 0x81, 0xf7];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn md5_simple_string_test(){
+ let hash = QtMd5::hash(get_simple_string_as_bytes());
+ let expected = vec![0x4b, 0x2e, 0x18, 0x22, 0x45, 0x43, 0xf3, 0x96, 0xee, 0x79, 0x53, 0x18, 0x90, 0x1b, 0xb9, 0x7f];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn md5_complex_string_test(){
+ let hash = QtMd5::hash(get_complex_string_as_bytes());
+ let expected = vec![0x70, 0x31, 0x35, 0xd9, 0x38, 0x55, 0x1d, 0x2a, 0xae, 0xfa, 0xd9, 0x38, 0x07, 0x91, 0x11, 0xfe ];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn sha1_simple_string_test(){
+ let hash = QtSha1::hash(get_simple_string_as_bytes());
+ let expected = vec![0xa1, 0x0a, 0x15, 0x18, 0x99, 0x29, 0x9d, 0xc7, 0xa6, 0x48, 0x36, 0x11, 0x44, 0xb3, 0x94, 0x09, 0x87, 0x3a, 0x39, 0xf3];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn sha1_complex_string_test(){
+ let hash = QtSha1::hash(get_complex_string_as_bytes());
+ let expected = vec![0x30, 0x79, 0xcd, 0xbc, 0x66, 0x09, 0xad, 0x24, 0x99, 0x44, 0xe5, 0x52, 0x25, 0xdf, 0xb4, 0x68, 0xfd, 0x5f, 0xb9, 0x8f ];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn sha256_simple_string_test(){
+ let hash = QtSha256::hash(get_simple_string_as_bytes());
+ let expected = vec![0xd4, 0xaf, 0x13, 0x6a, 0x87, 0x62, 0x12, 0xf1, 0x93, 0x7d, 0xd1, 0x71, 0xab, 0xa1, 0xfa, 0x3e, 0x3b, 0x8e, 0xc5, 0x68,
+ 0xed, 0x42, 0x46, 0x9d, 0xf0, 0x9b, 0xd0, 0xd8, 0xd8, 0x39, 0x09, 0x93];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn sha256_complex_string_test(){
+ let hash = QtSha256::hash(get_complex_string_as_bytes());
+ let expected = vec![0x01, 0x3d, 0x93, 0x17, 0x45, 0x18, 0x29, 0x41, 0x6a, 0x09, 0xb5, 0x65, 0x1b, 0x81, 0x32, 0x88, 0xce, 0x83, 0xad, 0x92,
+ 0x04, 0x0f, 0x24, 0x13, 0x57, 0x8d, 0xd1, 0xa5, 0xe8, 0x3a, 0x73, 0xaa ];
+ assert_eq!(hash.borrow(), expected);
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs
new file mode 100644
index 0000000..4cda572
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs
@@ -0,0 +1,102 @@
+use std::sync::mpsc::{Receiver, RecvError};
+use super::super::thread_messages::UiToHelper;
+
+pub(super) fn receive_and_get_newest_or_important_command(receiver : &Receiver<UiToHelper>) -> Result<NewestMostImportantCommand, RecvError> {
+ let first_message = receive_and_log_error(receiver)?;
+ Ok(get_newest_or_important_command(first_message, receiver.try_iter()))
+}
+
+pub(super) struct NewestMostImportantCommand(pub UiToHelper);
+
+trait GetNewerOrMoreImportantCommand {
+ type Output;
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output;
+ fn convert_first_command(self) -> Self::Output;
+}
+
+impl GetNewerOrMoreImportantCommand for UiToHelper {
+ type Output = NewestMostImportantCommand;
+
+ fn convert_first_command(self) -> Self::Output{
+ NewestMostImportantCommand(self)
+ }
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output {
+ match old.0 {
+ UiToHelper::Shutdown => old,
+ UiToHelper::GeneratePassword(_) => NewestMostImportantCommand(new),
+ }
+ }
+}
+
+
+fn get_newest_or_important_command<I, It >(first_message: I, other_messages: It) -> I::Output
+ where I: GetNewerOrMoreImportantCommand,
+ It: Iterator<Item=I>
+{
+ other_messages.fold(first_message.convert_first_command(), I::get_newer_or_more_important_command)
+}
+
+fn receive_and_log_error(receiver : &Receiver<UiToHelper>) -> Result<UiToHelper, RecvError> {
+ match receiver.recv() {
+ Ok(x) => Ok(x),
+ e => {
+ eprintln!("Connection to UI Thread closed unexpectedly.");
+ e
+ }
+ }
+}
+
+#[cfg(test)]
+mod message_parsing_tests {
+ use super::*;
+
+ #[derive(PartialEq, Debug)]
+ enum TestPrioritizedEnum {
+ Lowest(usize),
+ Highest(usize),
+ Medium(usize)
+ }
+ impl TestPrioritizedEnum{
+ fn get_prio(&self) -> usize{
+ match self {
+ TestPrioritizedEnum::Lowest(_) => 0,
+ TestPrioritizedEnum::Highest(_) => 2,
+ TestPrioritizedEnum::Medium(_) => 1,
+ }
+ }
+ }
+
+ struct TestPrioritizedEnumPrioResult(TestPrioritizedEnum);
+ impl GetNewerOrMoreImportantCommand for TestPrioritizedEnum {
+ type Output = TestPrioritizedEnumPrioResult;
+
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output {
+ if old.0.get_prio() > new.get_prio() { old } else { TestPrioritizedEnumPrioResult(new) }
+ }
+
+ fn convert_first_command(self) -> Self::Output {
+ TestPrioritizedEnumPrioResult(self)
+ }
+ }
+
+ #[test]
+ fn test_get_newest_or_important_command_test(){
+ let input1 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)];
+ let expected1 = TestPrioritizedEnum::Lowest(2);
+ let input2 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)];
+ let expected2 = TestPrioritizedEnum::Medium(0);
+ let input3 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Medium(1), TestPrioritizedEnum::Lowest(2)];
+ let expected3 = TestPrioritizedEnum::Medium(1);
+ let input4 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Medium(2)];
+ let expected4 = TestPrioritizedEnum::Medium(2);
+ let input5 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Highest(2), TestPrioritizedEnum::Medium(3)];
+ let expected5 = TestPrioritizedEnum::Highest(2);
+ let data = vec![(input1, expected1), (input2, expected2), (input3, expected3), (input4, expected4), (input5, expected5)];
+ for (input, expected) in data {
+ let mut it = input.into_iter();
+ let first = it.next().unwrap();
+ let result = get_newest_or_important_command(first,it);
+ assert_eq!(result.0, expected);
+ }
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/helperthread/mod.rs b/rust/src/implementation/passwordmaker/helperthread/mod.rs
new file mode 100644
index 0000000..7e3b587
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/mod.rs
@@ -0,0 +1,49 @@
+mod message_parsing;
+mod hashers;
+mod profile_to_domain;
+use std::sync::mpsc::{Receiver, SendError};
+use passwordmaker_rs::PasswordMaker;
+use profile_to_domain::{convert_hash_algorithm, convert_leet};
+use super::emittingsender::EmittingSender;
+use super::thread_messages::{HelperToUi, UiToHelper};
+use message_parsing::receive_and_get_newest_or_important_command;
+use hashers::PassFishHashers;
+
+pub(super) fn run<T: Fn()>(to_ui : &EmittingSender<HelperToUi, T>, from_ui : &Receiver<UiToHelper>) -> Result<(), SendError<HelperToUi>>{
+ println!("Helper Thread starting up.");
+
+ while let Ok(m) = receive_and_get_newest_or_important_command(from_ui)
+ {
+ //m is a product type, because we gather the latest command of each type. Easiest to deal with those would be using a group of ifs.
+ //for now the number of alternatives is rather small though, so let's match instead.
+ match m.0 {
+ UiToHelper::Shutdown => break,
+ UiToHelper::GeneratePassword(task) => {
+
+ to_ui.send(HelperToUi::GenerationStarted)?;
+ let hash_algorithm = convert_hash_algorithm(&task.generation_settings.hash_algorithm);
+ let use_leet = convert_leet(&task.generation_settings.leet);
+ let characters = &task.generation_settings.characters;
+ let username = &task.generation_settings.username;
+ let modifier = &task.generation_settings.modifier;
+ let password_length = task.generation_settings.password_length as usize;
+ let suffix = &task.generation_settings.suffix;
+ let prefix = &task.generation_settings.prefix;
+ type Pwm<'a> = PasswordMaker<'a, PassFishHashers>;
+ let pwm = Pwm::new(hash_algorithm, use_leet, characters, username, modifier, password_length, prefix, suffix);
+ let input = task.input;
+ let master_password = task.master_password;
+ let pwm = pwm.map_err(Into::into);
+ let password =
+ pwm.and_then(|pwm| pwm.generate(input, master_password).map_err(Into::into));
+
+ match password {
+ Ok(password) => to_ui.send(HelperToUi::Generated{password})?,
+ Err(error) => to_ui.send(HelperToUi::GenerationFailed{ error })?,
+ }
+ }
+ }
+ }
+ println!("Helper Thread shutting down.");
+ Ok(())
+}
diff --git a/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs
new file mode 100644
index 0000000..152ba8f
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs
@@ -0,0 +1,46 @@
+/// This whole module may look dumb, but that might change, once the public interface of passwordmaker_rs starts to deviate from the saved data.
+
+use passwordmaker_rs::{HashAlgorithm,UseLeetWhenGenerating,LeetLevel};
+
+pub(super) fn convert_hash_algorithm(stored_algo : &crate::implementation::profiles::HashAlgorithm) -> HashAlgorithm {
+ use crate::implementation::profiles::HashAlgorithm as PHashAlgorithm;
+ match stored_algo {
+ PHashAlgorithm::Md4 => HashAlgorithm::Md4,
+ PHashAlgorithm::HmacMd4 => HashAlgorithm::HmacMd4,
+ PHashAlgorithm::Md5 => HashAlgorithm::Md5,
+ PHashAlgorithm::Md5Version06 => HashAlgorithm::Md5Version06,
+ PHashAlgorithm::HmacMd5 => HashAlgorithm::HmacMd5,
+ PHashAlgorithm::HmacMd5Version06 => HashAlgorithm::HmacMd5Version06,
+ PHashAlgorithm::Sha1 => HashAlgorithm::Sha1,
+ PHashAlgorithm::HmacSha1 => HashAlgorithm::HmacSha1,
+ PHashAlgorithm::Sha256 => HashAlgorithm::Sha256,
+ PHashAlgorithm::HmacSha256 => HashAlgorithm::HmacSha256,
+ PHashAlgorithm::Ripemd160 => HashAlgorithm::Ripemd160,
+ PHashAlgorithm::HmacRipemd160 => HashAlgorithm::HmacRipemd160,
+ }
+}
+
+pub(super) fn convert_leet(stored_leet : &crate::implementation::profiles::UseLeetWhenGenerating) -> UseLeetWhenGenerating {
+ use crate::implementation::profiles::UseLeetWhenGenerating as PUseLeetWhenGenerating;
+ match stored_leet {
+ PUseLeetWhenGenerating::NotAtAll => UseLeetWhenGenerating::NotAtAll,
+ PUseLeetWhenGenerating::Before { level } => UseLeetWhenGenerating::Before { level: convert_leet_level(level) },
+ PUseLeetWhenGenerating::After { level } => UseLeetWhenGenerating::After { level: convert_leet_level(level) },
+ PUseLeetWhenGenerating::BeforeAndAfter { level } => UseLeetWhenGenerating::BeforeAndAfter { level: convert_leet_level(level) },
+ }
+}
+
+fn convert_leet_level(stored_level : &crate::implementation::profiles::LeetLevel) -> LeetLevel {
+ use crate::implementation::profiles::LeetLevel as PLeetLevel;
+ match stored_level {
+ PLeetLevel::One => LeetLevel::One,
+ PLeetLevel::Two => LeetLevel::Two,
+ PLeetLevel::Three => LeetLevel::Three,
+ PLeetLevel::Four => LeetLevel::Four,
+ PLeetLevel::Five => LeetLevel::Five,
+ PLeetLevel::Six => LeetLevel::Six,
+ PLeetLevel::Seven => LeetLevel::Seven,
+ PLeetLevel::Eight => LeetLevel::Eight,
+ PLeetLevel::Nine => LeetLevel::Nine,
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/mod.rs b/rust/src/implementation/passwordmaker/mod.rs
new file mode 100644
index 0000000..af0516c
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/mod.rs
@@ -0,0 +1,305 @@
+mod emittingsender;
+mod thread_messages;
+mod settings;
+mod helperthread;
+
+pub use self::settings::Settings;
+use passwordmaker_rs::{UrlParsing, ProtocolUsageMode, SettingsError, GenerationError};
+use crate::interface::ProfilesTrait;
+
+use std::cell::RefCell;
+use std::convert::TryFrom;
+use std::error::Error;
+use std::fmt::Display;
+use std::sync::mpsc::{Receiver, Sender, channel};
+use crate::implementation::pwm_macros::EnumVariantCount;
+
+use mockall_double::double;
+
+#[double]
+use crate::interface::PasswordMakerEmitter;
+use crate::interface::PasswordMakerTrait;
+use super::Profiles;
+use self::emittingsender::EmittingSender;
+use self::thread_messages::{GeneratePasswordTask, HelperToUi, UiToHelper, GenerationIssue};
+
+#[derive(EnumVariantCount, Clone, Copy)]
+enum GeneratorState{
+ MissingTextToUse,
+ MissingMasterPassword,
+ CharsetError,
+ GenerationCompleted,
+ Busy
+}
+
+pub struct PasswordMaker {
+ emit : PasswordMakerEmitter,
+ profiles : Profiles,
+ settings : Settings,
+ url : RefCell<String>,
+ used_text : RefCell<String>,
+ master_password : RefCell<String>,
+ generated_password : RefCell<String>,
+ generator_state : RefCell<GeneratorState>,
+ from_helper : Receiver<HelperToUi>,
+ to_helper : Sender<UiToHelper>,
+ helper_thread : Option<std::thread::JoinHandle<()>>
+}
+
+#[derive(Debug)]
+struct GeneratorStateFromIntError;
+impl Display for GeneratorStateFromIntError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f,"Invalid value for GeneratorStateFromIntError.")
+ }
+}
+impl Error for GeneratorStateFromIntError{}
+
+impl TryFrom<u8> for GeneratorState {
+ type Error = GeneratorStateFromIntError;
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(GeneratorState::GenerationCompleted),
+ 1 => Ok(GeneratorState::Busy),
+ 2 => Ok(GeneratorState::MissingTextToUse),
+ 3 => Ok(GeneratorState::MissingMasterPassword),
+ 4 => Ok(GeneratorState::CharsetError),
+ 5..=255 => Err(GeneratorStateFromIntError)
+ }
+ }
+}
+impl From<GeneratorState> for u8 {
+ fn from(s : GeneratorState) -> Self {
+ match s {
+ GeneratorState::MissingTextToUse => 2,
+ GeneratorState::GenerationCompleted => 0,
+ GeneratorState::Busy => 1,
+ GeneratorState::MissingMasterPassword => 3,
+ GeneratorState::CharsetError => 4,
+ }
+ }
+}
+
+impl PasswordMakerTrait for PasswordMaker {
+ fn new(emit : PasswordMakerEmitter, profiles : Profiles, settings : Settings) -> Self {
+ let stored_settings_data = settings.load();
+ if let Ok(x) = &stored_settings_data {
+ #[allow(clippy::cast_possible_truncation)]
+ profiles.set_current_profile(x.current_profile_index as u32);
+ }
+ let settings = if let Ok(x) = stored_settings_data { x.settings } else { settings };
+
+ let emit_clone = emit.clone();
+ let (to_ui,from_helper) = EmittingSender::emitting_channel(
+ move || {emit_clone.i_say_sexy_things_to_myself_while_im_dancing_changed();}
+ );
+ let (to_helper, from_ui) = channel();
+ let helper_thread = Some(std::thread::spawn(move || helperthread::run(&to_ui, &from_ui).expect("UI Thread hung up.") ));
+ PasswordMaker{emit, profiles, settings, from_helper, to_helper, helper_thread,
+ url: RefCell::new(String::new()), used_text: RefCell::new(String::new()), master_password: RefCell::new(String::new()), generated_password: RefCell::new(String::new()), generator_state: RefCell::new(GeneratorState::MissingTextToUse) }
+ }
+ fn emit(&self) -> &PasswordMakerEmitter {
+ &self.emit
+ }
+ fn profiles(&self) -> &Profiles {
+ &self.profiles
+ }
+ fn settings(&self) -> &Settings {
+ &self.settings
+ }
+ fn store_settings(&self) -> bool {
+ self.settings.store(self.profiles.current_profile() as usize).is_ok()
+ }
+ fn i_say_sexy_things_to_myself_while_im_dancing(&self) -> bool {
+ unreachable!()
+ }
+ fn set_i_say_sexy_things_to_myself_while_im_dancing(&self, _ : bool) {
+ if let Ok(m) = self.from_helper.try_recv() {
+ self.handle_message_from_helper(m);
+ } else {
+ println!("Spurious wakeup from helper thread. No message queued. Please investigate.");
+ }
+ }
+
+ fn generated_password<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.generated_password.borrow());
+ }
+
+ fn master_password<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.master_password.borrow());
+ }
+
+ fn set_master_password(&self, value: String) {
+ let different = {
+ let mut b = self.master_password.borrow_mut();
+ let different = *b != value;
+ if different {*b = value;}
+ different
+ };
+ if different {
+ self.update_generated_password();
+ self.emit().master_password_changed();
+ }
+ }
+
+ fn url<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.url.borrow());
+ }
+
+ fn set_url(&self, value: String) {
+ let different = {
+ let mut b = self.url.borrow_mut();
+ let different = *b != value;
+ if different { *b = value; }
+ different
+ };
+ if different {
+ self.update_used_text_from_url();
+ self.emit().url_changed();
+ }
+ }
+
+ fn used_text<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.used_text.borrow());
+ }
+
+ fn set_used_text(&self, value: String) {
+ let different = {
+ let mut b = self.used_text.borrow_mut();
+ let different = *b != value;
+ if different { *b = value; }
+ different
+ };
+ if different {
+ self.update_generated_password();
+ self.emit().used_text_changed();
+ }
+ }
+
+ fn generator_state(&self) -> u8 {
+ (*self.generator_state.borrow()).into()
+ }
+
+ fn profile_changed(&self) {
+ self.update_used_text_from_url();
+ }
+}
+
+impl Drop for PasswordMaker
+{
+ fn drop(&mut self) {
+ if self.to_helper.send(UiToHelper::Shutdown).is_err() {
+ eprintln!("Failed to tell worker thread to quit. Might need to kill the process.");
+ }
+ if let Some(j) = self.helper_thread.take() {
+ if j.join().is_err() {
+ eprintln!("Helper Thread crashed. This should not happen, so please report a bug.");
+ }
+ } else {
+ eprintln!("Somehow the information about the helper thread got lost, so we can't wait for it to quit. Might need to kill the process.");
+ }
+ }
+}
+
+impl PasswordMaker{
+ fn handle_message_from_helper(&self, message : HelperToUi){
+ //unless everything is terribly wrong, we are in the UI thread here.
+ match message {
+ HelperToUi::Generated{ password } => {
+ //println!("Password generated.");
+ {
+ *self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted;
+ *self.generated_password.borrow_mut() = password;
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ }
+ HelperToUi::GenerationFailed { error } => {
+ //println!("No password generated due to missing input.");
+ {
+ *self.generator_state.borrow_mut() = match error {
+ GenerationIssue::Settings(SettingsError::InsufficientCharset) => GeneratorState::CharsetError,
+ GenerationIssue::Input(GenerationError::MissingMasterPassword) => GeneratorState::MissingMasterPassword,
+ GenerationIssue::Input(GenerationError::MissingTextToUse) => GeneratorState::MissingTextToUse,
+ };
+ self.generated_password.borrow_mut().clear();
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ },
+ HelperToUi::GenerationStarted => {
+ //println!("Setting password generator state as busy.");
+ {
+ *self.generator_state.borrow_mut() = GeneratorState::Busy;
+ self.generated_password.borrow_mut().clear();
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ },
+ }
+ }
+ fn update_used_text_from_url(&self) {
+ let used_text = {
+ self.profiles.do_with_current_url_parsing_settings(
+ |settings| {
+ //have to convert saved data to corresponding runtime data.
+ let use_protocol = match (settings.use_protocol, settings.use_undefined_as_protocol_fallback) {
+ (true, false) => ProtocolUsageMode::Used,
+ (true, true) => ProtocolUsageMode::UsedWithUndefinedIfEmpty,
+ (false, _) => ProtocolUsageMode::Ignored,
+ };
+ let url_parsing = UrlParsing::new(
+ use_protocol,
+ settings.use_userinfo,
+ settings.use_subdomains,
+ settings.use_domain,
+ settings.use_port_path);
+ url_parsing.parse(&self.url.borrow())
+ }
+ )
+ };
+ { *self.used_text.borrow_mut() = used_text.unwrap_or_else(|e| e.to_string()); }
+ self.emit().used_text_changed();
+ self.update_generated_password(); //intentionally unconditional.
+ }
+ fn update_generated_password(&self) {
+ let generation_settings = self.profiles.get_copy_current_generation_settings();
+ match generation_settings {
+ Ok(generation_settings) => {
+ self.to_helper.send(
+ UiToHelper::GeneratePassword(
+ GeneratePasswordTask{
+ input: self.used_text.borrow().clone(),
+ master_password: self.master_password.borrow().clone(),
+ generation_settings }
+ )
+ ).expect("Helper thread no longer listening. Unrecoverable.");
+ }
+ Err(error) => {
+ {*self.generated_password.borrow_mut() = error.to_string();}
+ {*self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted;}
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod passwordmaker_tests {
+ use std::convert::TryInto;
+
+ use super::*;
+ #[test]
+ fn generator_state_reciprocity() {
+ for i in 0..(GeneratorState::variant_count() as u8) {
+ let g : GeneratorState = i.try_into().unwrap();
+ let j : u8 = g.into();
+ assert_eq!(j,i);
+ }
+ for i in (GeneratorState::variant_count() as u8)..=255 {
+ let g : Result<GeneratorState,_> = i.try_into();
+ assert!(g.is_err());
+ }
+ }
+}
diff --git a/rust/src/implementation/passwordmaker/settings.rs b/rust/src/implementation/passwordmaker/settings.rs
new file mode 100644
index 0000000..487ffea
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/settings.rs
@@ -0,0 +1,159 @@
+use crate::interface::SettingsTrait;
+use crate::implementation::{LoadError, StoreError};
+
+use serde::{Serialize, Deserialize};
+use std::default::Default;
+use std::cell::RefCell;
+
+use mockall_double::double;
+
+#[double]
+use crate::interface::SettingsEmitter;
+
+#[derive(Serialize, Deserialize)]
+struct SettingsSaveData {
+ current_profile_index : usize,
+ settings : SettingsData
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct SettingsData{
+ clear_generated_password_seconds : Option<u32>,
+ clear_master_password_seconds : Option<u32>,
+ hide_generated_password : bool
+}
+
+impl Default for SettingsData {
+ fn default() -> Self {
+ SettingsData {
+ clear_generated_password_seconds : Some(60),
+ clear_master_password_seconds : Some(300),
+ hide_generated_password : false
+ }
+ }
+}
+
+pub struct Settings {
+ emit : SettingsEmitter,
+ data : RefCell<SettingsData>
+}
+
+pub struct SettingLoadResult {
+ pub settings : Settings,
+ pub current_profile_index : usize
+}
+
+impl Settings {
+ /// Loads the data from disk and merges them with the current Settings object. Only use during startup, as it does NOT emit.
+ pub(super) fn load(&self) -> Result<SettingLoadResult, LoadError>{
+ super::super::get_config_folder()
+ .map(|p| p.join("settings"))
+ .ok_or(LoadError::Xdg)
+ .and_then(|p| std::fs::read_to_string(p).map_err(Into::into))
+ .and_then(|s| toml::from_str(&s).map_err(Into::into))
+ .map(|sd| self.merge_with_loaded_data_no_emit(sd))
+ }
+ pub(super) fn store(&self, current_profile_index : usize) -> Result<(), StoreError>{
+ toml::to_string(&SettingsSaveData{current_profile_index, settings : self.data.borrow().clone()})
+ .map_err(Into::into)
+ .and_then(|s| Self::write_serialized_settings_data(&s))
+ }
+
+ ///Private. Should only be used during load().
+ fn merge_with_loaded_data_no_emit(&self, save_data: SettingsSaveData) -> SettingLoadResult {
+ SettingLoadResult {
+ settings : Settings { emit : self.emit.clone(), data : RefCell::new(save_data.settings) },
+ current_profile_index : save_data.current_profile_index
+ }
+ }
+ fn write_serialized_settings_data(data : &str) -> Result<(), StoreError> {
+ super::super::get_config_folder()
+ .ok_or(StoreError::Xdg)
+ .and_then(|p| std::fs::create_dir_all(p.clone()).map_err(Into::into).map(|()| p))
+ .map(|p| p.join("settings"))
+ .and_then(|f| std::fs::write(f, data).map_err(Into::into))
+ }
+}
+
+impl SettingsTrait for Settings {
+ fn new(emit: SettingsEmitter) -> Self {
+ Settings {emit, data : RefCell::new(SettingsData::default())}
+ }
+ fn emit(&self) -> &SettingsEmitter{
+ &self.emit
+ }
+ fn clear_generated_password_seconds(&self) -> Option<u32> {
+ self.data.borrow().clear_generated_password_seconds
+ }
+ fn set_clear_generated_password_seconds(&self, value: Option<u32>) {
+ let changed = self.data.borrow().clear_generated_password_seconds != value;
+ self.data.borrow_mut().clear_generated_password_seconds = value;
+ if changed {
+ self.emit().clear_generated_password_seconds_changed();
+ }
+ }
+ fn clear_master_password_seconds(&self) -> Option<u32> {
+ self.data.borrow().clear_master_password_seconds
+ }
+ fn set_clear_master_password_seconds(&self, value: Option<u32>) {
+ let changed = self.data.borrow().clear_master_password_seconds != value;
+ self.data.borrow_mut().clear_master_password_seconds = value;
+ if changed {
+ self.emit().clear_master_password_seconds_changed();
+ }
+ }
+
+ fn hide_generated_password(&self) -> bool {
+ self.data.borrow().hide_generated_password
+ }
+
+ fn set_hide_generated_password(&self, value: bool) {
+ let changed = {
+ let mut v = self.data.borrow_mut();
+ let changed = v.hide_generated_password != value;
+ v.hide_generated_password = value;
+ changed
+ };
+ if changed {
+ self.emit().hide_generated_password_changed();
+ }
+ }
+}
+
+#[cfg(test)]
+mod settings_test{
+ use super::*;
+
+ #[test]
+ fn merge_with_loaded_data_test(){
+ let mut emit = SettingsEmitter::new();
+ emit.expect_clone().return_once(||{
+ let mut e2 = SettingsEmitter::new();
+ e2.expect_clone().never();
+ e2.expect_clear_generated_password_seconds_changed().never();
+ e2.expect_clear_master_password_seconds_changed().never();
+ e2.expect_hide_generated_password_changed().never();
+ e2
+ }).once();
+ emit.expect_clear_generated_password_seconds_changed().never();
+ emit.expect_clear_master_password_seconds_changed().never();
+ emit.expect_hide_generated_password_changed().never();
+ let old_settings = Settings {
+ emit,
+ data: RefCell::new(SettingsData::default()),
+ };
+ let save_data = SettingsSaveData{
+ current_profile_index: 2,
+ settings: SettingsData {
+ clear_generated_password_seconds: Some(600),
+ clear_master_password_seconds: None,
+ hide_generated_password: true
+ },
+ };
+ let result = old_settings.merge_with_loaded_data_no_emit(save_data);
+ assert_eq!(result.current_profile_index, 2);
+ assert_eq!(result.settings.data.borrow().clear_generated_password_seconds, Some(600));
+ assert_eq!(result.settings.data.borrow().clear_master_password_seconds, None);
+ assert_eq!(result.settings.data.borrow().hide_generated_password, true);
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/thread_messages.rs b/rust/src/implementation/passwordmaker/thread_messages.rs
new file mode 100644
index 0000000..bba3dfa
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/thread_messages.rs
@@ -0,0 +1,41 @@
+use std::sync::Arc;
+use passwordmaker_rs::{GenerationError,SettingsError};
+use crate::implementation::profiles::GenerationSettings;
+
+pub(super) enum HelperToUi {
+ Generated {
+ password : String,
+ },
+ GenerationFailed {
+ error : GenerationIssue
+ },
+ GenerationStarted
+}
+
+pub(super) struct GeneratePasswordTask{
+ pub(crate) input : String,
+ pub(crate) master_password : String,
+ pub(crate) generation_settings : Arc<GenerationSettings>
+}
+
+pub(super) enum UiToHelper {
+ Shutdown,
+ GeneratePassword(GeneratePasswordTask),
+}
+
+pub(super) enum GenerationIssue {
+ Settings(SettingsError),
+ Input(GenerationError),
+}
+
+impl From<SettingsError> for GenerationIssue {
+ fn from(s: SettingsError) -> Self {
+ GenerationIssue::Settings(s)
+ }
+}
+
+impl From<GenerationError> for GenerationIssue {
+ fn from(g: GenerationError) -> Self {
+ GenerationIssue::Input(g)
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/profiles.rs b/rust/src/implementation/profiles.rs
new file mode 100644
index 0000000..5d9bbfc
--- /dev/null
+++ b/rust/src/implementation/profiles.rs
@@ -0,0 +1,1127 @@
+use std::convert::{From, Into, TryFrom, TryInto};
+use std::cell::RefCell;
+use std::fmt::Display;
+use std::sync::Arc;
+use serde::{Serialize, Deserialize};
+use mockall_double::double;
+#[cfg(test)]
+use crate::implementation::pwm_macros::*;
+
+#[double]
+use crate::interface::{ProfilesEmitter, ProfilesList};
+use crate::interface::ProfilesTrait;
+
+use crate::implementation::{LoadError, StoreError};
+
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
+#[cfg_attr(test, derive(EnumVariantCount))]
+pub(crate) enum HashAlgorithm {
+ Md4,
+ HmacMd4,
+ Md5,
+ Md5Version06,
+ HmacMd5,
+ HmacMd5Version06,
+ Sha1,
+ HmacSha1,
+ Sha256,
+ HmacSha256,
+ Ripemd160,
+ HmacRipemd160,
+}
+
+impl Display for HashAlgorithm {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ HashAlgorithm::Md4 => write!(f,"MD4"),
+ HashAlgorithm::HmacMd4 => write!(f, "MD4-HMAC"),
+ HashAlgorithm::Md5 => write!(f, "MD5"),
+ HashAlgorithm::Md5Version06 => write!(f, "MD5 PWM Version 0.6"),
+ HashAlgorithm::HmacMd5 => write!(f, "MD5-HMAC"),
+ HashAlgorithm::HmacMd5Version06 => write!(f, "MD5-HMAC PWM Version 0.6"),
+ HashAlgorithm::Sha1 => write!(f,"SHA-1"),
+ HashAlgorithm::HmacSha1 => write!(f, "SHA-1-HMAC"),
+ HashAlgorithm::Sha256 => write!(f, "SHA-256"),
+ HashAlgorithm::HmacSha256 => write!(f, "SHA-256-HMAC"),
+ HashAlgorithm::Ripemd160 => write!(f, "RIPEMD-160"),
+ HashAlgorithm::HmacRipemd160 => write!(f, "RIPEMD-160-HMAC"),
+ }
+ }
+}
+
+impl From<HashAlgorithm> for u8 {
+ fn from(h : HashAlgorithm) -> u8 {
+ h as u8
+ }
+}
+impl TryFrom<u8> for HashAlgorithm {
+ type Error = ();
+ fn try_from(i : u8) -> Result<HashAlgorithm,()> {
+ match i {
+ 0 => { Ok(HashAlgorithm::Md4) }
+ 1 => { Ok(HashAlgorithm::HmacMd4) }
+ 2 => { Ok(HashAlgorithm::Md5) }
+ 3 => { Ok(HashAlgorithm::Md5Version06) }
+ 4 => { Ok(HashAlgorithm::HmacMd5) }
+ 5 => { Ok(HashAlgorithm::HmacMd5Version06) }
+ 6 => { Ok(HashAlgorithm::Sha1) }
+ 7 => { Ok(HashAlgorithm::HmacSha1) }
+ 8 => { Ok(HashAlgorithm::Sha256) }
+ 9 => { Ok(HashAlgorithm::HmacSha256) }
+ 10 => {Ok(HashAlgorithm::Ripemd160) }
+ 11 => {Ok(HashAlgorithm::HmacRipemd160)}
+ _ => { Err(()) }
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
+#[cfg_attr(test, derive(EnumVariantCount))]
+pub(crate) enum LeetLevel {
+ One,
+ Two,
+ Three,
+ Four,
+ Five,
+ Six,
+ Seven,
+ Eight,
+ Nine,
+}
+
+impl From<LeetLevel> for u8 {
+ fn from(l : LeetLevel) -> u8 {
+ l as u8 + 1
+ }
+}
+impl TryFrom<u8> for LeetLevel {
+ type Error = ();
+ fn try_from(i : u8) -> Result<LeetLevel,()> {
+ match i {
+ 1 => { Ok(LeetLevel::One) }
+ 2 => { Ok(LeetLevel::Two) }
+ 3 => { Ok(LeetLevel::Three) }
+ 4 => { Ok(LeetLevel::Four) }
+ 5 => { Ok(LeetLevel::Five) }
+ 6 => { Ok(LeetLevel::Six) }
+ 7 => { Ok(LeetLevel::Seven) }
+ 8 => { Ok(LeetLevel::Eight) }
+ 9 => { Ok(LeetLevel::Nine) }
+ _ => { Err(()) }
+ }
+ }
+}
+
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+#[serde(tag = "UseLeet")]
+pub(crate) enum UseLeetWhenGenerating {
+ NotAtAll,
+ Before {
+ level : LeetLevel,
+ },
+ After {
+ level : LeetLevel,
+ },
+ BeforeAndAfter {
+ level : LeetLevel,
+ },
+}
+
+impl UseLeetWhenGenerating {
+ pub(crate) fn get_leet_level(&self) -> Option<LeetLevel> {
+ match self {
+ UseLeetWhenGenerating::NotAtAll => { None }
+ UseLeetWhenGenerating::Before { level }
+ | UseLeetWhenGenerating::After { level }
+ | UseLeetWhenGenerating::BeforeAndAfter { level } => { Some(*level) }
+ }
+ }
+ pub(crate) fn set_leet_level(&mut self, lvl : LeetLevel) -> Result<(),()> {
+ match self {
+ UseLeetWhenGenerating::NotAtAll => { Err(()) }
+ UseLeetWhenGenerating::Before { level }
+ | UseLeetWhenGenerating::After { level }
+ | UseLeetWhenGenerating::BeforeAndAfter { level} => { *level = lvl; Ok(()) }
+ }
+ }
+}
+
+#[allow(clippy::struct_excessive_bools)]
+#[derive(Serialize, Deserialize, Clone)]
+pub(super) struct UrlParsingSettings {
+ pub(super) use_protocol : bool,
+ pub(super) use_userinfo : bool,
+ pub(super) use_subdomains : bool,
+ pub(super) use_domain : bool,
+ pub(super) use_port_path : bool,
+ pub(super) use_undefined_as_protocol_fallback : bool
+}
+
+impl std::default::Default for UrlParsingSettings {
+ fn default() -> Self {
+ Self {
+ use_protocol: false,
+ use_userinfo : false,
+ use_subdomains: false,
+ use_domain: true,
+ use_port_path: false ,
+ use_undefined_as_protocol_fallback: true
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub(crate) struct GenerationSettings {
+ pub(crate) password_length : u32,
+ pub(crate) username : String,
+ pub(crate) modifier : String,
+ pub(crate) characters : String,
+ pub(crate) prefix : String,
+ pub(crate) suffix : String,
+ pub(crate) hash_algorithm : HashAlgorithm,
+ pub(crate) leet : UseLeetWhenGenerating,
+}
+
+impl std::default::Default for GenerationSettings {
+ fn default() -> Self {
+ Self {
+ leet : UseLeetWhenGenerating::NotAtAll,
+ hash_algorithm : HashAlgorithm::Md5,
+ password_length : 8,
+ username : String::new(),
+ modifier : String::new(),
+ characters : String::from(
+ r#"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()_-+={}|[]\:";'<>?,./"#
+ ),
+ prefix : String::new(),
+ suffix : String::new(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, Default)]
+struct ProfileSettings {
+ generation_settings : Arc<GenerationSettings>,
+ url_parsing_settings : UrlParsingSettings,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Profile {
+ name : String,
+ settings : ProfileSettings,
+}
+
+#[derive(Serialize, Deserialize)]
+struct StoredProfiles {
+ profiles : Vec<Profile>,
+}
+
+impl std::default::Default for StoredProfiles {
+ fn default() -> Self {
+ Self {
+ profiles : vec![Profile {
+ name : String::from("Default"),
+ settings : ProfileSettings::default()
+ }]
+ }
+ }
+}
+
+impl StoredProfiles {
+ fn load() -> Result<Self, LoadError> {
+ super::get_config_folder()
+ .map(|p| p.join("profiles"))
+ .ok_or(LoadError::Xdg)
+ .and_then(|p| std::fs::read_to_string(p).map_err(Into::into))
+ .and_then(|s| toml::from_str(&s).map_err(Into::into))
+ }
+
+ fn store(&self) -> Result<(), StoreError> {
+ toml::to_string(self).map_err(Into::into)
+ .and_then(|s| Self::write_serialized_profile_data(&s))
+ }
+ fn write_serialized_profile_data(data : &str) -> Result<(), StoreError> {
+ super::get_config_folder()
+ .ok_or(StoreError::Xdg)
+ .and_then(|p| std::fs::create_dir_all(p.clone()).map_err(Into::into).map(|()| p))
+ .map(|p| p.join("profiles"))
+ .and_then(|f| std::fs::write(f, data).map_err(Into::into))
+ }
+}
+
+pub struct Profiles {
+ emit : ProfilesEmitter,
+ model : ProfilesList,
+
+ data : RefCell<StoredProfiles>,
+
+ current_profile_idx : RefCell<usize>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) enum ProfileAccessError {
+ CurrentProfileOutOfBounds
+}
+impl Display for ProfileAccessError{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ProfileAccessError::CurrentProfileOutOfBounds => write!(f, "error reading current profile, please reselect profile")
+ }
+ }
+}
+impl std::error::Error for ProfileAccessError{}
+
+impl Profiles {
+ fn notify_current_profile_changed(&self) {
+ self.emit().current_profile_changed();
+ self.emit().current_profile_name_changed();
+ }
+
+ fn model(&self) -> &ProfilesList {
+ #[cfg(debug_assertions)]
+ assert!(!self.is_any_borrowed());
+ &self.model
+ }
+
+ pub(super) fn do_with_current_url_parsing_settings<F, R>(&self, operation : F) -> Result<R, ProfileAccessError>
+ where F : FnOnce(&UrlParsingSettings) -> R
+ {
+ self.data.borrow().profiles.get(*self.current_profile_idx.borrow())
+ .map(|p| &p.settings.url_parsing_settings).map(operation)
+ .ok_or(ProfileAccessError::CurrentProfileOutOfBounds)
+ }
+
+ pub(crate) fn get_copy_current_generation_settings(&self) -> Result<Arc<GenerationSettings>, ProfileAccessError> {
+ self.data.borrow().profiles.get(*self.current_profile_idx.borrow())
+ .map(|p| p.settings.generation_settings.clone())
+ .ok_or(ProfileAccessError::CurrentProfileOutOfBounds)
+ }
+
+ #[cfg(debug_assertions)]
+ fn is_any_borrowed(&self) -> bool {
+ self.data.try_borrow_mut().is_err() ||
+ self.current_profile_idx.try_borrow_mut().is_err()
+ }
+}
+
+impl ProfilesTrait for Profiles {
+ fn new(emit : ProfilesEmitter, model : ProfilesList) -> Self {
+ let data = RefCell::new(StoredProfiles::load().unwrap_or_default());
+ Profiles { emit, model, data, current_profile_idx : RefCell::new(0) }
+ }
+ fn emit(&self) -> &ProfilesEmitter {
+ #[cfg(debug_assertions)]
+ assert!(!self.is_any_borrowed());
+ &self.emit
+ }
+
+ fn row_count(&self) -> usize {
+ self.data.borrow().profiles.len()
+ }
+ fn insert_rows(&self, row : usize, count : usize) -> bool {
+ let max_index = self.row_count();
+ let valid_index = row <= self.data.borrow().profiles.len();
+ if valid_index {
+ self.model().begin_insert_rows(row, row + count - 1);
+ let new_profiles = (0..count)
+ .map(|c| format!("New Profile {}", max_index + c))
+ .map(|name| Profile { name, settings : ProfileSettings::default() });
+ self.data.borrow_mut().profiles.splice(row..row,new_profiles);
+ self.model().end_insert_rows();
+ //if the current profile is at or beyond row, we need to update it, so it still points
+ //to the same profile
+ if *self.current_profile_idx.borrow() >= row {
+ *self.current_profile_idx.borrow_mut() += count;
+ //no need to notify about field changes - those are still the same. Only the
+ //internal index changed.
+ self.emit().current_profile_changed();
+ }
+ }
+ valid_index
+ }
+ fn remove_rows(&self, row : usize, count : usize) -> bool {
+ let valid_index = row + count <= self.row_count();
+ let at_least_one_left = self.row_count() > count;
+ let operation_allowed = valid_index && at_least_one_left;
+ if operation_allowed {
+ self.model().begin_remove_rows(row, row + count - 1);
+ self.data.borrow_mut().profiles.drain(row..(row+count));
+ //we have to update the current profile index before we complete the row removal.
+ //this is to ensure the index is not going invalid.
+ //here we can have a destructive operation: the current index might have been removed.
+ //in that case set it to 0.
+ let current_idx = *self.current_profile_idx.borrow();
+ if row + count <= current_idx {
+ *self.current_profile_idx.borrow_mut() -= count;
+ //same as in the insert_rows case here: The field values are still the same, only
+ //the index changed.
+ self.emit().current_profile_changed();
+ }
+ else if row <= current_idx {
+ *self.current_profile_idx.borrow_mut() = 0;
+ self.notify_current_profile_changed();
+ }
+ self.model().end_remove_rows();
+ }
+ operation_allowed
+ }
+
+ fn characters<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow().profiles.get(index)
+ .map(|p| &*p.settings.generation_settings.characters)
+ .unwrap_or_default());
+ }
+ fn set_characters(&self, index : usize, val : String) -> bool {
+ if let Some(chars) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).characters)
+ .filter(|c| **c != val) {
+ *chars = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn hash_algorithm(&self, index : usize) -> u8 {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.generation_settings.hash_algorithm.into())
+ .unwrap_or_default()
+ }
+ fn set_hash_algorithm(&self, index : usize, val : u8) -> bool {
+ let hash = val.try_into().ok();
+ let mut borrow = self.data.borrow_mut();
+ let profile = borrow.profiles.get_mut(index);
+ if let Some((p, h)) = profile.zip(hash)
+ .map(|(p, h)| (&mut Arc::make_mut(&mut p.settings.generation_settings).hash_algorithm, h))
+ .filter(|(p, h)| *p != h) {
+ *p = h;
+ true
+ } else {
+ false
+ }
+ }
+ fn leet_level(&self, index : usize) -> u8 {
+ self.data.borrow().profiles.get(index)
+ .and_then(|p| p.settings.generation_settings.leet.get_leet_level())
+ .map(Into::into)
+ .unwrap_or_default()
+ }
+ fn set_leet_level(&self, index : usize, level : u8) -> bool {
+ let level = level.try_into().ok();
+ let mut borrow = self.data.borrow_mut();
+ let profile = borrow.profiles.get_mut(index);
+ if let Some((p, l)) = profile.zip(level)
+ .map(|(p,l)| (&mut Arc::make_mut(&mut p.settings.generation_settings).leet, l))
+ .filter(|(p,l)| p.get_leet_level() != Some(*l)) {
+ p.set_leet_level(l).is_ok()
+ } else {
+ false
+ }
+ }
+ fn modifier<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow().profiles.get(index)
+ .map(|p| &*p.settings.generation_settings.modifier)
+ .unwrap_or_default());
+ }
+ fn set_modifier(&self, index : usize, val : String) -> bool {
+ if let Some(m) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).modifier)
+ .filter(|m| **m != val) {
+ *m = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn name<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow().profiles.get(index)
+ .map_or("profile index invalid",|p| &*p.name));
+ }
+ fn set_name(&self, index : usize, val : String) -> bool {
+ let changed = {
+ if let Some(n) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.name)
+ .filter(|n| **n != val) {
+ *n = val;
+ true
+ } else {
+ false
+ }
+ };
+ if changed && index == *self.current_profile_idx.borrow() {
+ self.emit().current_profile_name_changed();
+ }
+ changed
+ }
+ fn password_length(&self, index : usize) -> u32 {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.generation_settings.password_length)
+ .unwrap_or_default()
+ }
+ fn set_password_length(&self, index : usize, len : u32) -> bool {
+ if let Some(pl) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).password_length)
+ .filter(|pl| **pl != len) {
+ *pl = len;
+ true
+ } else {
+ false
+ }
+ }
+ fn prefix<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow().profiles.get(index)
+ .map(|p| &*p.settings.generation_settings.prefix)
+ .unwrap_or_default());
+ }
+ fn set_prefix(&self, index : usize, pref : String) -> bool {
+ if let Some(p) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).prefix)
+ .filter(|p| **p != pref) {
+ *p = pref;
+ true
+ } else {
+ false
+ }
+ }
+ fn suffix<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow_mut().profiles.get(index)
+ .map(|p| &*p.settings.generation_settings.suffix)
+ .unwrap_or_default());
+ }
+ fn set_suffix(&self, index : usize, suf : String) -> bool {
+ if let Some(s) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).suffix)
+ .filter(|s| **s != suf) {
+ *s=suf;
+ true
+ } else {
+ false
+ }
+ }
+ fn use_domain(&self, index : usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map_or(true,|p| p.settings.url_parsing_settings.use_domain)
+ }
+ fn set_use_domain(&self, index : usize, val : bool) -> bool {
+ if let Some(d) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_domain)
+ .filter(|d| **d != val) {
+ *d = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn use_leet(&self, index : usize) -> u8 {
+ self.data.borrow().profiles.get(index)
+ .map(|p| match p.settings.generation_settings.leet {
+ UseLeetWhenGenerating::NotAtAll => { 0 }
+ UseLeetWhenGenerating::Before{..} => { 1 }
+ UseLeetWhenGenerating::After {..} => { 2 }
+ UseLeetWhenGenerating::BeforeAndAfter{..} => { 3 }
+ })
+ .unwrap_or_default()
+ }
+ fn set_use_leet(&self, index : usize, l : u8) -> bool {
+ let mut borrow = self.data.borrow_mut();
+ let profile = borrow.profiles.get_mut(index);
+ let level = |profile : Option<&Profile>| {
+ profile.as_ref().and_then(|p| p.settings.generation_settings.leet.get_leet_level())
+ .unwrap_or(LeetLevel::One)
+ };
+ let use_leet = match l {
+ 0 => { Some(UseLeetWhenGenerating::NotAtAll) }
+ 1 => { Some(UseLeetWhenGenerating::Before { level : level(profile.as_deref()) }) }
+ 2 => { Some(UseLeetWhenGenerating::After { level : level(profile.as_deref()) }) }
+ 3 => { Some(UseLeetWhenGenerating::BeforeAndAfter{ level : level(profile.as_deref()) }) }
+ _ => { None }
+ };
+ if let Some((p, l)) = profile.zip(use_leet)
+ .map(|(p,l)| (&mut Arc::make_mut(&mut p.settings.generation_settings).leet, l))
+ .filter(|(p,l)| *p != l) {
+ *p = l;
+ true
+ } else {
+ false
+ }
+ }
+ fn use_port_path(&self, index : usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.url_parsing_settings.use_port_path)
+ .unwrap_or_default()
+ }
+ fn set_use_port_path(&self, index : usize, val : bool) -> bool {
+ if let Some(pp) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_port_path)
+ .filter(|pp| **pp != val) {
+ *pp = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn use_protocol(&self, index : usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.url_parsing_settings.use_protocol)
+ .unwrap_or_default()
+ }
+ fn set_use_protocol(&self, index : usize, val : bool) -> bool {
+ if let Some(pr) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_protocol)
+ .filter(|pr| **pr != val) {
+ *pr = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn use_subdomains(&self, index : usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.url_parsing_settings.use_subdomains)
+ .unwrap_or_default()
+ }
+ fn set_use_subdomains(&self, index : usize, val : bool) -> bool {
+ if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_subdomains)
+ .filter(|sd| **sd != val) {
+ *sd = val;
+ true
+ } else {
+ false
+ }
+ }
+ fn username<F : FnOnce(&str)>(&self, index : usize, setter : F) {
+ setter(self.data.borrow().profiles.get(index)
+ .map(|p| &*p.settings.generation_settings.username)
+ .unwrap_or_default());
+ }
+ fn set_username(&self, index : usize, name : String) -> bool {
+ if let Some(u) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).username)
+ .filter(|u| **u != name) {
+ *u = name;
+ true
+ } else {
+ false
+ }
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ fn current_profile(&self) -> u32 {
+ *self.current_profile_idx.borrow() as u32
+ }
+ fn set_current_profile(&self, value : u32) {
+ let valid_index = self.row_count() > value as usize;
+ let does_change = *self.current_profile_idx.borrow() != value as usize;
+ if valid_index && does_change {
+ *self.current_profile_idx.borrow_mut() = value as usize;
+ self.notify_current_profile_changed();
+ }
+ }
+ fn current_profile_name<F : FnOnce(&str)>(&self, setter : F) {
+ self.name(*self.current_profile_idx.borrow(), setter);
+ }
+
+ fn store(&self) -> bool {
+ self.data.borrow().store()
+ .map_err(|e| println!("{}", e))
+ .is_ok()
+ }
+
+ fn use_undefined_as_protocol_fallback(&self, index: usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.url_parsing_settings.use_undefined_as_protocol_fallback)
+ .unwrap_or_default()
+ }
+
+ fn set_use_undefined_as_protocol_fallback(&self, index: usize, val: bool) -> bool {
+ if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_undefined_as_protocol_fallback)
+ .filter(|sd| **sd != val) {
+ *sd = val;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn use_user_info(&self, index: usize) -> bool {
+ self.data.borrow().profiles.get(index)
+ .map(|p| p.settings.url_parsing_settings.use_userinfo)
+ .unwrap_or_default()
+ }
+
+ fn set_use_user_info(&self, index: usize, val: bool) -> bool {
+ if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index)
+ .map(|p| &mut p.settings.url_parsing_settings.use_userinfo)
+ .filter(|sd| **sd != val) {
+ *sd = val;
+ true
+ } else {
+ false
+ }
+ }
+}
+
+#[cfg(test)]
+mod profiles_tests {
+ use super::*;
+ fn get_test_profiles_three_entries(emit : ProfilesEmitter, model : ProfilesList) -> Profiles {
+ Profiles {
+ emit,
+ model,
+ data : RefCell::new( StoredProfiles {
+ profiles : vec![
+ Profile {
+ name : String::from("First"),
+ settings : ProfileSettings::default(),
+ },
+ Profile {
+ name : String::from("Second"),
+ settings : ProfileSettings::default(),
+ },
+ Profile {
+ name : String::from("Third"),
+ settings : ProfileSettings::default(),
+ },
+ ],
+ }),
+ current_profile_idx : RefCell::new(1),
+ }
+ }
+ #[test]
+ fn hash_algo_reciprocicity_test() {
+ for i in 0..(HashAlgorithm::variant_count() as u8) {
+ let x : HashAlgorithm = i.try_into().unwrap();
+ let y = x.into();
+ assert_eq!(i,y);
+ }
+ let z: Result<HashAlgorithm,_> = (HashAlgorithm::variant_count() as u8).try_into();
+ assert!(z.is_err());
+ }
+ #[test]
+ fn leet_level_reciprocicity_test() {
+ for i in 1..=(LeetLevel::variant_count() as u8) {
+ let x : LeetLevel = i.try_into().unwrap();
+ let y = x.into();
+ assert_eq!(i,y);
+ }
+ let z : Result<LeetLevel,_> = (1+LeetLevel::variant_count() as u8).try_into();
+ assert!(z.is_err());
+ let a : Result<LeetLevel,_> = 0.try_into();
+ assert!(a.is_err());
+ }
+ #[test]
+ fn set_name_test() {
+ let (emit_nothing, model_doesnt_emit) = expect_no_emission();
+ let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit);
+ //profiles_assert_on_emit has profile 1 set as active. We can therefore modify the name of 0 (or 2) without emission.
+ let should_change = profiles_assert_on_emit.set_name(0, "SomethingIJustMadeUp".into());
+ assert!(should_change);
+ profiles_assert_on_emit.name(0, |x| {assert_eq!(x, "SomethingIJustMadeUp")});
+ }
+ #[test]
+ fn set_current_profile_test() {
+ {
+ //setting to same index should not do anything.
+ let (emit_nothing, model_doesnt_emit) = expect_no_emission();
+ let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit);
+ profiles_assert_on_emit.set_current_profile(profiles_assert_on_emit.current_profile());
+ }
+ {
+ //setting to different index should emit both name and index changed.
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().return_once(||{()}).once();
+ emit.expect_current_profile_name_changed().return_once(||{()}).once();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ let profiles_wants_one_name_change = get_test_profiles_three_entries(emit, list);
+ let previous_profile = profiles_wants_one_name_change.current_profile();
+ profiles_wants_one_name_change.set_current_profile(previous_profile + 1);
+ assert_eq!(previous_profile + 1, profiles_wants_one_name_change.current_profile());
+ }
+ {
+ //Last, but not least, even if the _name_ of the profiles is the same, we WANT to emit current_profile_name_changed.
+ //This is because the UI is also functioning as Controller, so every profile change (even if it's the same name) should refresh the
+ //corresponding UI.
+
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().return_once(||{()}).once();
+ emit.expect_current_profile_name_changed().return_once(||{()}).once();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ let p = Profiles {
+ emit,
+ model :list,
+ data : RefCell::new( StoredProfiles {
+ profiles : vec![
+ Profile {
+ name : String::from("Twin"),
+ settings : ProfileSettings::default(),
+ },
+ Profile {
+ name : String::from("Twin"),
+ settings : ProfileSettings::default(),
+ },
+ ],
+ }),
+ current_profile_idx : RefCell::new(1),
+ };
+ p.set_current_profile(0);
+ assert_eq!(p.current_profile(), 0);
+ }
+ }
+ #[test]
+ fn current_profile_name_test() {
+ //let's check if setting the same value we already have emits. Hopefully not. Set the index 1 profile's name to "Second"
+ {
+ let (emit_nothing, model_doesnt_emit) = expect_no_emission();
+ let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit);
+ let should_not_change = profiles_assert_on_emit.set_name(1,"Second".into());
+ assert!(!should_not_change);
+ }
+
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().never();
+ emit.expect_current_profile_name_changed().return_once(||{()}).once();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ let profiles_wants_one_name_change = get_test_profiles_three_entries(emit, list);
+ let should_change2 = profiles_wants_one_name_change.set_name(1, "Agadlagugu".into());
+ assert!(should_change2);
+ profiles_wants_one_name_change.name(1, |x| {assert_eq!(x, "Agadlagugu")});
+ }
+ #[test]
+ fn insert_rows_before_current_test() {
+ //if we add a row before the current, only, current_profile should emit. No need to emit profile_name, as the data is still the same.
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().return_once(||{()}).once();
+ emit.expect_current_profile_name_changed().never();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().return_once(|_,_|{()}).once();
+ list.expect_end_insert_rows().return_once(||{()}).once();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ //must make sure that index changes, but data remains same
+ let profile_tests_insert_before_current = get_test_profiles_three_entries(emit, list);
+ let get_current_name_copy = || { let mut res = String::new(); profile_tests_insert_before_current.current_profile_name(|x| res = String::from(x)); res};
+ let old_name = get_current_name_copy();
+ let old_index = profile_tests_insert_before_current.current_profile();
+ let worked = profile_tests_insert_before_current.insert_rows(1, 1);
+ assert!(worked);
+ let new_name = get_current_name_copy();
+ let new_index = profile_tests_insert_before_current.current_profile();
+ assert_eq!(old_name, new_name);
+ assert_eq!(new_index, old_index +1);
+ }
+ #[test]
+ fn insert_rows_after_current_test() {
+ //if we add a row after current, no signals other than insert_rows related ones should emit:
+ //if we add a row before the current, only, current_profile should emit. No need to emit profile_name, as the data is still the same.
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().never();
+ emit.expect_current_profile_name_changed().never();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().return_once(|_,_|{()}).once();
+ list.expect_end_insert_rows().return_once(||{()}).once();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ //must make sure that index changes, but data remains same
+ let profile_tests_insert_before_current = get_test_profiles_three_entries(emit, list);
+ let get_current_name_copy = || { let mut res = String::new(); profile_tests_insert_before_current.current_profile_name(|x| res = String::from(x)); res};
+ let old_name = get_current_name_copy();
+ let old_index = profile_tests_insert_before_current.current_profile();
+ let worked = profile_tests_insert_before_current.insert_rows(3, 1);
+ assert!(worked);
+ let new_name = get_current_name_copy();
+ let new_index = profile_tests_insert_before_current.current_profile();
+ assert_eq!(old_name, new_name);
+ assert_eq!(new_index, old_index);
+ }
+ #[test]
+ fn remove_rows_before_current_test() {
+ //if we remove a row before the current, only current_profile should emit. No need to emit profile_name, as the data is still the same.
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().return_once(||{()}).once();
+ emit.expect_current_profile_name_changed().never();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().return_once(|_,_|{()}).once();
+ list.expect_end_remove_rows().return_once(||{()}).once();
+ //must make sure that index changes, but data remains same
+ let profile_tests_remove_before_current = get_test_profiles_three_entries(emit, list);
+ let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_before_current.current_profile_name(|x| res = String::from(x)); res};
+ let old_name = get_current_name_copy();
+ let old_index = profile_tests_remove_before_current.current_profile();
+ let worked = profile_tests_remove_before_current.remove_rows(0, 1);
+ assert!(worked);
+ let new_name = get_current_name_copy();
+ let new_index = profile_tests_remove_before_current.current_profile();
+ assert_eq!(old_name, new_name);
+ assert_eq!(new_index, old_index - 1);
+ }
+ #[test]
+ fn remove_rows_containign_current_test() {
+ //if we remove the current row, both, index and name will change and index should be zero.
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().return_once(||{()}).once();
+ emit.expect_current_profile_name_changed().return_once(||{()}).once();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().return_once(|_,_|{()}).once();
+ list.expect_end_remove_rows().return_once(||{()}).once();
+ //must make sure that index changes, but data remains same
+ let profile_tests_remove_current = get_test_profiles_three_entries(emit, list);
+ let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_current.current_profile_name(|x| res = String::from(x)); res};
+ let old_name = get_current_name_copy();
+ let old_index = profile_tests_remove_current.current_profile();
+ let worked = profile_tests_remove_current.remove_rows(1, 1);
+ assert!(worked);
+ let new_name = get_current_name_copy();
+ let new_index = profile_tests_remove_current.current_profile();
+ assert_ne!(old_name, new_name);
+ assert_eq!(new_index, 0);
+ assert_ne!(new_index, old_index);
+ }
+ #[test]
+ fn remove_rows_after_current_test() {
+ //only remove rows emit, no profile related emit
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().never();
+ emit.expect_current_profile_name_changed().never();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().return_once(|_,_|{()}).once();
+ list.expect_end_remove_rows().return_once(||{()}).once();
+ //must make sure that index changes, but data remains same
+ let profile_tests_remove_after_current = get_test_profiles_three_entries(emit, list);
+ let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_after_current.current_profile_name(|x| res = String::from(x)); res};
+ let old_name = get_current_name_copy();
+ let old_index = profile_tests_remove_after_current.current_profile();
+ let worked = profile_tests_remove_after_current.remove_rows(2, 1);
+ assert!(worked);
+ let new_name = get_current_name_copy();
+ let new_index = profile_tests_remove_after_current.current_profile();
+ assert_eq!(old_name, new_name);
+ assert_eq!(new_index, old_index);
+ }
+ // Leet level needs its own test, as setting leet level depends on use_leet.
+ #[test]
+ fn set_leet_level_test() {
+ let (emit, model) = expect_no_emission();
+ //we're not going through the new() method, to avoid touching the filesystem.
+ let profiles = get_test_profiles_three_entries(emit, model);
+
+ //remember the old values in other rows to confimr they aren't touched
+ let oldvals = [profiles.leet_level(0), profiles.leet_level(2)];
+
+ //make sure that leet isn't enabled.
+ assert_eq!(profiles.use_leet(1), u8::default());
+ //make sure we are changing something.
+ assert_ne!(profiles.leet_level(1), 2);
+ //assure that nothing happens if we try to change leet level with leet disabled.
+ let changed = profiles.set_leet_level(1,2);
+ assert!(!changed);
+ assert_eq!(profiles.use_leet(1), u8::default());
+
+ //enable leet.
+ let changed = profiles.set_use_leet(1,1);
+ assert!(changed);
+ //make sure we are changing something.
+ assert_ne!(profiles.leet_level(1), 2);
+ //actually change something:
+ let changed = profiles.set_leet_level(1,2);
+ assert!(changed);
+ assert_eq!(profiles.leet_level(1), 2);
+ //check that other values are unaffected
+ assert_eq!(profiles.leet_level(0), oldvals[0]);
+ assert_eq!(profiles.leet_level(2), oldvals[1]);
+ //IMPORTANT: check that assigning the same value returns not-changed.
+ let changed = profiles.set_leet_level(1,2);
+ assert!(!changed);
+ //disable leet again and make sure leet_level returns 0 again.
+ let changed = profiles.set_use_leet(1,0);
+ assert!(changed);
+ assert_eq!(profiles.leet_level(1), u8::default());
+ }
+
+ // The remaining properties should (at the time of writing) not emit.
+ // This means we can use one and the same test for all of them :-)
+ // Except that complex types have a different getter syntax...
+ // Oh well.
+ fn expect_no_emission() -> (ProfilesEmitter, ProfilesList) {
+ let mut emit = ProfilesEmitter::new();
+ emit.expect_clone().never();
+ emit.expect_current_profile_changed().never();
+ emit.expect_current_profile_name_changed().never();
+ emit.expect_new_data_ready().never();
+ let mut list = ProfilesList::new();
+ list.expect_layout_about_to_be_changed().never();
+ list.expect_layout_changed().never();
+ list.expect_data_changed().never();
+ list.expect_begin_reset_model().never();
+ list.expect_end_reset_model().never();
+ list.expect_begin_insert_rows().never();
+ list.expect_end_insert_rows().never();
+ list.expect_begin_move_rows().never();
+ list.expect_end_move_rows().never();
+ list.expect_begin_remove_rows().never();
+ list.expect_end_remove_rows().never();
+ (emit, list)
+ }
+ macro_rules! simple_setter_tests {
+ ( $( $test_name:ident, $setter:ident, $getter:ident, $val:literal ),* ) => {
+ $(#[test]
+ fn $test_name() {
+ let (emit, model) = expect_no_emission();
+ //we're not going through the new() method, to avoid touching the filesystem.
+ let profiles = get_test_profiles_three_entries(emit, model);
+
+ //remember the old values in other rows to confimr they aren't touched
+ let oldvals = [profiles.$getter(0), profiles.$getter(2)];
+
+ //make sure we are changing something.
+ assert_ne!(profiles.$getter(1), $val);
+ //assure that something changed
+ let changed = profiles.$setter(1,$val);
+ assert!(changed);
+ assert_eq!(profiles.$getter(1), $val);
+ //check that other values are unaffected
+ assert_eq!(profiles.$getter(0), oldvals[0]);
+ assert_eq!(profiles.$getter(2), oldvals[1]);
+ //IMPORTANT: check that assigning the same value returns not-changed.
+ let changed = profiles.$setter(1,$val);
+ assert!(!changed);
+ })*
+ };
+ }
+ macro_rules! string_setter_tests {
+ ( $( $test_name:ident, $setter:ident, $getter:ident, $val:literal ),* ) => {
+ $(#[test]
+ fn $test_name() {
+ let (emit, model) = expect_no_emission();
+ //we're not going through the new() method, to avoid touching the filesystem.
+ let profiles = get_test_profiles_three_entries(emit, model);
+
+ //remember the old values in other rows to confirm they aren't touched.
+ let mut oldvals = [String::new(), String::new()];
+ profiles.$getter(0, |x| { oldvals[0] = String::from(x); });
+ profiles.$getter(2, |x| { oldvals[1] = String::from(x); });
+ //make sure we are changing something.
+ profiles.$getter(1, |x| { assert_ne!(x, $val); });
+ //assure that something changed:
+ let changed = profiles.$setter(1,String::from($val));
+ assert!(changed);
+ profiles.$getter(1, |x| { assert_eq!(x, $val); });
+ //check that the other values are unaffected.
+ profiles.$getter(0, |x| { assert_eq!(oldvals[0], x); });
+ profiles.$getter(2, |x| { assert_eq!(oldvals[1], x); });
+ //IMPORTANT: check that assigning the same value returns not-changed.
+ let changed = profiles.$setter(1,String::from($val));
+ assert!(!changed);
+ })*
+ };
+ }
+ string_setter_tests!(
+ set_characters_test,set_characters,characters,"ABCDEFG",
+ set_modifier_test, set_modifier, modifier, "ab",
+ set_prefix_test, set_prefix, prefix, "xy",
+ set_suffix_test, set_suffix, suffix, "zw",
+ set_username_test, set_username, username, "Zargothrax"
+ );
+ simple_setter_tests!(
+ set_hash_algorithm_test, set_hash_algorithm, hash_algorithm, 5,
+ set_password_length_test, set_password_length, password_length, 12,
+ set_use_domain_test, set_use_domain, use_domain, false,
+ set_use_leet_test, set_use_leet, use_leet, 1,
+ set_use_port_path_test, set_use_port_path, use_port_path, true,
+ set_use_protocol_test, set_use_protocol, use_protocol, true,
+ set_use_subdomains_test, set_use_subdomains, use_subdomains, true,
+ set_use_user_info_test, set_use_user_info, use_user_info, true,
+ set_use_undefined_as_protocol_fallback_test, set_use_undefined_as_protocol_fallback, use_undefined_as_protocol_fallback, false
+ );
+}
diff --git a/rust/src/implementation/pwm_macros.rs b/rust/src/implementation/pwm_macros.rs
new file mode 100644
index 0000000..158e4cd
--- /dev/null
+++ b/rust/src/implementation/pwm_macros.rs
@@ -0,0 +1,27 @@
+extern crate passwordmaker_macros;
+pub use self::passwordmaker_macros::*;
+
+pub trait EnumVariantCount {
+ fn variant_count() -> usize;
+}
+
+#[cfg(test)]
+mod pwm_macro_tests {
+ use super::*;
+
+ #[allow(dead_code)]
+ enum Nest{ A, B }
+ #[allow(dead_code)]
+ #[derive(EnumVariantCount)]
+ enum TestNum {
+ A(usize),
+ B,
+ C(Nest),
+ D,
+ E{a : usize, b: f64}
+ }
+ #[test]
+ fn enum_variant_count_test(){
+ assert_eq!(TestNum::variant_count(), 5);
+ }
+} \ No newline at end of file