+ PassFish is a native re-implementation of PasswordMaker for Sailfish OS.
+ It aims to be mostly compatible to the Javascript Edition.
+
+
+ All credit for the development of the PasswordMaker Pro algorithm goes to the original authors of PasswordMaker Pro, Miquel Burns and Eric H. Jung.
+
+
+ As PassFish is not merely a port, but a full reimplementation from scratch, compatibility is not guaranteed. While the original source code was
+ used as a guideline during implementation, the underlying technology is vastly different. Common use cases are tested by integration tests, but some
+ edge cases might have been missed. In case you encounter such an issue, please report it on the
+ github issue tracker of the project.
+
+
+ Speaking of integration tests: The hash algorithms were not re-implemented for this project in order to reduce the risk of introducing bugs. Instead the
+ QCryptographicHash API is used where possible, and where not, the implementation from the RustCrypto Hashes repository is utilized (see list of dependencies below).
+
+
+ This program consists of two parts: The application itself (\"PassFish\"), and a Rust crate that contains the
+ implementation of the business logic (\"passwordmaker‑rs\").
+ This is important, as the two parts use different licenses. PassFish is published under the GPLv3 license, while passwordmaker‑rs is published under LGPLv3. Please check the linked github pages for more details.
+
+
+ This program utilises several third party libraries. This list is kept up-to-date to the best of my knowledge. Only direct dependencies are listed,
+ for transitive dependencies please see the linked websites. Similarly, the source code for those third-party dependencies that are published under an
+ open source license can be found on the linked websites. To my knowledge the only non-open-source dependency is Silica. The listed licenses are just those
+ used by this project, most libraries are available under multiple licenses. Please see the libraries' websites for details.
+ These libraries are linked dynamically:
+
+ PassFish uses a modified version, which can be found in the mockall_support branch.
+
+
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
+
+ ")
+ }
+ }
+
+ VerticalScrollDecorator { flickable: aboutPage}
+ }
+}
diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml
new file mode 100644
index 0000000..b860e67
--- /dev/null
+++ b/qml/pages/MainPage.qml
@@ -0,0 +1,241 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import "../components"
+
+Page {
+ id: mainPage
+
+ readonly property var isBackground : Qt.application.state
+
+ onIsBackgroundChanged: {
+ if(isBackground === Qt.ApplicationInactive) {
+ mainFlickable.restart_timers();
+ }
+ }
+
+ SilicaFlickable {
+ id: mainFlickable
+ anchors.fill: parent
+ contentHeight: column.height
+
+ function store_settings_with_error_message() {
+ var worked = passwordmaker.store_settings();
+ if(!worked) {
+ storeFailureNotice.show();
+ }
+ }
+ function restart_timers() {
+ if(autoClearMasterPasswordTimer.enabled && passwordmaker.master_password.length > 0) {
+ autoClearMasterPasswordTimer.restart();
+ }
+ if(autoClearGeneratedPasswordTimer.enabled && (passwordmaker.used_text.length > 0 || passwordmaker.url.length > 0)) {
+ autoClearGeneratedPasswordTimer.restart();
+ }
+ }
+
+ PullDownMenu {
+ MenuItem {
+ text: qsTr("App Settings")
+ onClicked: {
+ var pg = pageStack.animatorPush(Qt.resolvedUrl("SettingsEditor.qml"),
+ {
+ clear_generated_password : passwordmaker.settings.clear_generated_password_seconds,
+ clear_master_password : passwordmaker.settings.clear_master_password_seconds,
+ hide_generated_password : passwordmaker.settings.hide_generated_password
+ });
+ pg.pageCompleted.connect(function(pg) {
+ pg.accepted.connect(function() {
+ passwordmaker.settings.clear_generated_password_seconds
+ =pg.clear_generated_password;
+ passwordmaker.settings.clear_master_password_seconds
+ =pg.clear_master_password;
+ passwordmaker.settings.hide_generated_password
+ =pg.hide_generated_password;
+
+ mainFlickable.store_settings_with_error_message()
+ })
+ })
+ }
+ }
+ MenuItem {
+ text: qsTr("About")
+ onClicked: pageStack.animatorPush(Qt.resolvedUrl("AboutPage.qml"))
+ }
+ }
+ VerticalScrollDecorator {}
+ Column {
+ id: column
+
+ width: mainPage.width
+ spacing: Theme.paddingLarge
+ PageHeader {
+ title: qsTr("PassFish")
+ }
+
+
+ ValueButton {
+ id: profileButton
+ label: qsTr("Profile")
+ value: passwordmaker.profiles.current_profile_name
+
+ onClicked: pageStack.animatorPush(Qt.resolvedUrl("ProfilesPage.qml"))
+ }
+ TextField {
+ id: url
+ width: parent.width
+
+ text: passwordmaker.url
+ inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
+
+ label: qsTr("URL")
+ placeholderText: qsTr("URL")
+ EnterKey.iconSource: "image://theme/icon-m-enter-next"
+ EnterKey.onClicked: masterPassword.focus = true
+ Binding {
+ target: passwordmaker
+ property: "url"
+ value: url.text
+ }
+ }
+ PasswordField {
+ id: masterPassword
+ width: parent.width
+
+ text: passwordmaker.master_password
+
+ label: qsTr("Master Password")
+ placeholderText: qsTr("Master Password")
+ EnterKey.iconSource: "image://theme/icon-m-enter-close"
+ EnterKey.onClicked: focus = false
+ Binding {
+ target: passwordmaker
+ property: "master_password"
+ value: masterPassword.text
+ }
+ }
+ TextField {
+ id: usedText
+ width: parent.width
+
+ inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
+
+ label: qsTr("Used Text")
+ onFocusChanged: if(focus) text = passwordmaker.used_text
+ onPlaceholderTextChanged: if(!focus) text = ""
+ placeholderText: passwordmaker.used_text === "" ? qsTr("Used Text") : passwordmaker.used_text
+ EnterKey.iconSource: "image://theme/icon-m-enter-close"
+ EnterKey.onClicked: focus = false
+ Binding {
+ target: passwordmaker
+ property: "used_text"
+ value: usedText.text
+ when: usedText.focus
+ }
+ }
+ Separator {
+ width: parent.width
+ horizontalAlignment:Qt.AlignHCenter
+ color: Theme.secondaryColor
+ }
+ Row{
+ width: parent.width
+ PasswordField {
+ id: generatedPassword
+ width: parent.width - copyToClipboard.width
+
+
+ function password_text_from_generator_state(g, t, e) {
+ switch(g){
+ case 0 :
+ return e === TextInput.Normal ? t : t.replace(/./g, passwordCharacter);
+ case 1 :
+ return qsTr("Generating");
+ case 2 :
+ return qsTr("Missing text to use");
+ case 3 :
+ return qsTr("Missing master password");
+ case 4 :
+ return qsTr("Error in profile character list");
+ default:
+ return "";
+ }
+ }
+
+ passwordEchoMode: passwordmaker.settings.hide_generated_password
+ ? TextInput.Password : TextInput.Normal
+ showEchoModeToggle: passwordmaker.settings.hide_generated_password
+ readOnly: true
+ focusOnClick: true
+ label: qsTr("Generated Password")
+ hideLabelOnEmptyField: false
+ placeholderText:
+ password_text_from_generator_state(
+ passwordmaker.generator_state
+ , passwordmaker.generated_password
+ , echoMode)
+
+ onFocusChanged: if(focus) text = passwordmaker.generated_password; else text = "";
+ placeholderColor: color
+
+ BusyIndicator {
+ id: busy
+ parent: null
+ size: BusyIndicatorSize.Small
+ running: passwordmaker.generator_state === 1
+ }
+ states: State {
+ when: passwordmaker.generator_state === 1
+ PropertyChanges {
+ target: generatedPassword
+ rightItem: busy
+ }
+ }
+ }
+ IconButton{
+ id: copyToClipboard
+ enabled: passwordmaker.generator_state === 0 && passwordmaker.generated_password.length > 0
+ icon.source: "image://theme/icon-m-clipboard"
+ onClicked: Clipboard.text = passwordmaker.generated_password;
+ }
+ }
+ Timer {
+ id: autoClearMasterPasswordTimer
+ property bool enabled: typeof passwordmaker.settings.clear_master_password_seconds !== 'undefined'
+ interval: passwordmaker.settings.clear_master_password_seconds*1000 || 10000000
+ running: false
+ onTriggered: if(enabled)
+ passwordmaker.master_password = "";
+ }
+ Timer {
+ id: autoClearGeneratedPasswordTimer
+ property bool enabled: typeof passwordmaker.settings.clear_generated_password_seconds !== 'undefined'
+ interval: passwordmaker.settings.clear_generated_password_seconds*1000 || 10000000
+ running: false
+ onTriggered: if(enabled){
+ passwordmaker.used_text = "";
+ passwordmaker.url = "";
+ }
+ }
+ }
+ NoticeOptional {
+ id: storeFailureNotice
+ text: qsTr("Saving settings failed.")
+ useNotificationFallback: true
+ }
+
+ Binding{
+ target: autoClearMasterPasswordTimer
+ property: "running"
+ value: false
+ when: !autoClearMasterPasswordTimer.enabled || isBackground === Qt.ApplicationActive
+ }
+ Binding{
+ target: autoClearGeneratedPasswordTimer
+ property: "running"
+ value: false
+ when: !autoClearGeneratedPasswordTimer.enabled || isBackground === Qt.ApplicationActive
+ }
+ }
+ readonly property int current_profile_index : passwordmaker.profiles.current_profile
+ onCurrent_profile_indexChanged: {mainFlickable.store_settings_with_error_message()}
+}
diff --git a/qml/pages/ProfileEditor.qml b/qml/pages/ProfileEditor.qml
new file mode 100644
index 0000000..5dd28e0
--- /dev/null
+++ b/qml/pages/ProfileEditor.qml
@@ -0,0 +1,346 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import "../components"
+import PWM 1.0
+
+Dialog {
+ id: profileEditor
+
+ property alias profileName: profileNameField.text
+ property alias useProtocol: protocolField.checked
+ property alias useSubdomain: subdomainField.checked
+ property alias useDomain: domainField.checked
+ property alias usePortPath: portPathField.checked
+ property alias useUserInfo: userInfoField.checked
+ property alias useDefaultFallbackForProtocol : useDefaultFallbackForProtocolField.checked
+ property alias passwordLength: passwordLengthSlider.value
+ property alias hashAlgorithm: hashAlgorithmComboBox.currentIndex
+ property alias useLeet : useLeetComboBox.currentIndex
+ property alias leetLevel : leetLevelSlider.value
+ property alias characters : charactersField.text
+ property alias username : usernameField.text
+ property alias modifier : modifierField.text
+ property alias prefix : prefixField.text
+ property alias suffix : suffixField.text
+
+ canAccept: {
+ profileNameField.acceptableInput
+ && charactersField.acceptableInput
+ && urlPartsColumn.anySelected
+ }
+
+ onAcceptBlocked: {
+ if(!urlPartsColumn.anySelected) {
+ urlNotice.show();
+ }
+ else if(!profileNameField.acceptableInput) {
+ nameNotice.show();
+ }
+ else if(!charactersField.acceptableInput) {
+ charactersNotice.show();
+ }
+ }
+
+
+ SilicaFlickable {
+ anchors.fill: parent
+ contentHeight: column.height
+
+ VerticalScrollDecorator {}
+
+ Column {
+ id: column
+ width: parent.width
+ bottomPadding: Theme.paddingLarge
+ DialogHeader {
+ title: qsTr("Edit Profile")
+ }
+ TextField {
+ id: profileNameField
+ width: parent.width
+ errorHighlight: !acceptableInput
+
+ //description doesn't work on Sailfish 3. Use label instead if unavailable.
+ readonly property bool descriptionAvailable : typeof(description) !== "undefined"
+ label: !descriptionAvailable && errorHighlight ? qsTr("Required Field") : qsTr("Profile Name")
+ hideLabelOnEmptyField: descriptionAvailable
+ placeholderText: qsTr("Profile Name")
+
+ Binding {
+ target: profileNameField
+ property: "description"
+ value: profileNameField.errorHighlight ? qsTr("Required Field") : ""
+ when: profileNameField.descriptionAvailable
+ }
+
+ validator: RegExpValidator{regExp: /.+/}
+ //It doesn't make much sense to send the focus to the unrelated and rarely used fields waaaaay at the bottom and skip most relevant fields. Rather close the keyboard.
+ EnterKey.iconSource: "image://theme/icon-m-enter-close"
+ EnterKey.onClicked: focus = false
+ }
+ NoticeOptional {
+ id: nameNotice
+ text: qsTr("Profile name required.")
+ useNotificationFallback: false
+ }
+ Column {
+ id: urlPartsColumn
+ width: parent.width
+ topPadding: Theme.paddingLarge
+ bottomPadding: Theme.paddingLarge
+
+ property bool anySelected : (useProtocol || useSubdomain || useDomain || usePortPath || useUserInfo)
+
+ TextSwitch {
+ id: protocolField
+ text: qsTr("Use Protocol")
+ description: qsTr("Include URL protocol (e.g. \"http://\")")
+ palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor
+ highlighted: down || !urlPartsColumn.anySelected
+ }
+ TextSwitch {
+ id: useDefaultFallbackForProtocolField
+ visible: protocolField.checked
+ text: qsTr("Use \"undefined\" if protocol is missing")
+ description: qsTr("Enable to mimic weird behaviour of PasswordMaker Pro.")
+ palette.highlightColor : down ? Theme.highlightColor : Theme.errorColor
+ highlighted: down
+ }
+ TextSwitch {
+ id: userInfoField
+ text: qsTr("Use Userinfo")
+ description: qsTr("Include userinfo (e.g \"jane_doe:12345\"")
+ palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor
+ highlighted: down || !urlPartsColumn.anySelected
+ }
+ TextSwitch {
+ id: subdomainField
+ text: qsTr("Use Subomain(s)")
+ description: qsTr("Include URL subdomain(s) (e.g. \"www.\")")
+ palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor
+ highlighted: down || !urlPartsColumn.anySelected
+ }
+ TextSwitch {
+ id: domainField
+ text: qsTr("Use Domain")
+ description: qsTr("Include URL domain (e.g. \"example.com\")")
+ palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor
+ highlighted: down || !urlPartsColumn.anySelected
+ }
+ TextSwitch {
+ id: portPathField
+ text: qsTr("Use Port/Path")
+ description: qsTr("Include port and path (e.g \":8080/file\")")
+ palette.highlightColor : down || urlPartsColumn.anySelected ? Theme.highlightColor : Theme.errorColor
+ highlighted: down || !urlPartsColumn.anySelected
+
+ }
+ NoticeOptional {
+ id: urlNotice
+ text: qsTr("At least one URL part required.")
+ useNotificationFallback: false
+ }
+ }
+ Column {
+ width: parent.width
+ topPadding: Theme.paddingLarge
+ bottomPadding: Theme.paddingLarge
+ Slider {
+ id: passwordLengthSlider
+ minimumValue: 1
+ maximumValue: 50
+ stepSize: 1
+ width: parent.width
+ valueText : value
+ label: qsTr("Password Length")
+ }
+ ComboBox {
+ id: hashAlgorithmComboBox
+ label: qsTr("Hash Algorithm")
+ menu: ContextMenu {
+ MenuItem { text: "MD4" }
+ MenuItem { text: "HMAC-MD4" }
+ MenuItem { text: "MD5" }
+ MenuItem { text: qsTr("MD5 Version 0.6") }
+ MenuItem { text: "HMAC-MD5" }
+ MenuItem { text: qsTr("HMAC-MD5 Version 0.6") }
+ MenuItem { text: "SHA-1" }
+ MenuItem { text: "HMAC-SHA-1" }
+ MenuItem { text: "SHA-256" }
+ MenuItem { text: "HMAC-SHA-256" }
+ MenuItem { text: "RIPEMD-160" }
+ MenuItem { text: "HMAC-RIPEMD-160" }
+ }
+ }
+ ComboBox {
+ id: useLeetComboBox
+ label: qsTr("Use L33t")
+ menu: ContextMenu {
+ MenuItem { text: qsTr("not at all") }
+ MenuItem { text: qsTr("before generating") }
+ MenuItem { text: qsTr("after generating") }
+ MenuItem { text: qsTr("before and after generating") }
+ }
+ }
+ Slider {
+ id: leetLevelSlider
+ minimumValue: 1
+ maximumValue: 9
+ stepSize: 1
+ width: parent.width
+ valueText: value
+ visible: useLeetComboBox.currentIndex > 0
+ label: qsTr("Leet Level")
+ }
+ }
+ Column {
+ enabled: hashAlgorithmComboBox.currentIndex != 3 && hashAlgorithmComboBox.currentIndex != 5
+ width: parent.width
+ topPadding: Theme.paddingLarge
+ bottomPadding: Theme.paddingLarge
+ ListModel {
+ id: defaultCharacterValues
+ ListElement {
+ name: qsTr("Default Characters")
+ chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Alphanumeric")
+ chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Letters only")
+ chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Numbers only")
+ chars: "0123456789"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Special only")
+ chars: "`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Hex")
+ chars: "0123456789abcdef"
+ userFacing: true
+ }
+ ListElement {
+ name: qsTr("Custom")
+ chars: ""
+ userFacing: false
+ }
+ }
+ ComboBox {
+ id: defaultCharactersMenu
+
+ function matchIndex(text) {
+ for(var i=0; i < defaultCharacterValues.count - 1;++i)
+ if(defaultCharacterValues.get(i).chars === text)
+ return i;
+ return defaultCharacterValues.count - 1;
+ }
+
+ Binding {
+ target: defaultCharactersMenu
+ property: "currentIndex"
+ value: defaultCharactersMenu.matchIndex(charactersField.text)
+ }
+
+ label: qsTr("Characters Preset")
+ menu: ContextMenu {
+ Repeater {
+ model: defaultCharacterValues
+ MenuItem {
+ text: name
+ visible: userFacing
+ onClicked: if(userFacing) charactersField.text = chars
+ }
+ }
+ }
+ }
+ TextField {
+ id: charactersField
+ width: parent.width
+ errorHighlight: !acceptableInput
+
+ //description doesn't work on Sailfish 3. Use label instead if unavailable.
+ readonly property bool descriptionAvailable : typeof(description) !== "undefined"
+ label: !descriptionAvailable && errorHighlight ? qsTr("Need at least 2 characters.") : qsTr("Characters")
+ hideLabelOnEmptyField: descriptionAvailable
+ placeholderText: qsTr("Characters")
+ validator: GraphemeCountValidator { minGraphemeCount: 2 }
+
+ //The text fields below are conceptually different. Close the keyboard.
+ EnterKey.iconSource: "image://theme/icon-m-enter-close"
+ EnterKey.onClicked: focus = false
+
+ Binding {
+ target: charactersField
+ property: "description"
+ value: charactersField.errorHighlight ? qsTr("Need at least 2 characters.") : ""
+ when: charactersField.descriptionAvailable
+ }
+ states: State{
+ name: "locked"
+ when: hashAlgorithmComboBox.currentIndex === 3 || hashAlgorithmComboBox.currentIndex === 5
+ PropertyChanges {
+ target: charactersField
+ text: "0123456789abcdef"
+
+ }
+ }
+
+ }
+ NoticeOptional {
+ id: charactersNotice
+ text: qsTr("Need at least 2 characters.")
+ useNotificationFallback: false
+ }
+ }
+ Column {
+ width: parent.width
+ topPadding: Theme.paddingLarge
+ bottomPadding: Theme.paddingLarge
+ TextField {
+ id: usernameField
+ width: parent.width
+ label: qsTr("Username")
+ placeholderText: qsTr("Username")
+ EnterKey.iconSource: "image://theme/icon-m-enter-next"
+ EnterKey.onClicked: modifierField.focus = true
+ }
+ TextField {
+ id: modifierField
+ width: parent.width
+ label: qsTr("Modifier")
+ placeholderText: qsTr("Modifier")
+ EnterKey.iconSource: "image://theme/icon-m-enter-next"
+ EnterKey.onClicked: prefixField.focus = true
+ }
+ TextField {
+ id: prefixField
+ width: parent.width
+ label: qsTr("Prefix")
+ placeholderText: qsTr("Prefix")
+ EnterKey.iconSource: "image://theme/icon-m-enter-next"
+ EnterKey.onClicked: suffixField.focus = true
+ }
+ TextField {
+ id: suffixField
+ width: parent.width
+ label: qsTr("Suffix")
+ placeholderText: qsTr("Suffix")
+ //There are many non-keyboard-input switches on this page. Do not let the user confirm using keyboard.
+ EnterKey.iconSource: "image://theme/icon-m-enter-close"
+ EnterKey.onClicked: focus = false
+ }
+ }
+ }
+ }
+}
diff --git a/qml/pages/ProfilesPage.qml b/qml/pages/ProfilesPage.qml
new file mode 100644
index 0000000..a2df8af
--- /dev/null
+++ b/qml/pages/ProfilesPage.qml
@@ -0,0 +1,129 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import "../components"
+
+Page {
+ id: profilesSelector
+ SilicaListView {
+ id : profilesView
+ anchors.fill: parent
+ model : passwordmaker.profiles
+
+ function store_profile_with_error_message() {
+ var worked = profilesView.model.store();
+ if(!worked) {
+ storeFailureNotice.show();
+ }
+ }
+
+ PullDownMenu {
+ MenuItem {
+ text: qsTr("Add Profile")
+ onClicked: {
+ profilesView.model.insertRows(profilesView.model.rowCount(),1);
+ profilesView.store_profile_with_error_message();
+ }
+ }
+ }
+ header: PageHeader {
+ title: qsTr("Select/Edit Profiles")
+ }
+ delegate: ListItem {
+ id: delegate
+ width: parent.width
+ ListView.onAdd: AddAnimation {
+ target: delegate
+ }
+ ListView.onRemove: RemoveAnimation {
+ target: delegate
+ }
+ Label {
+ x: Theme.horizontalPageMargin
+ text: name
+ anchors.verticalCenter: parent.verticalCenter
+ color: index === profilesView.model.current_profile ? Theme.highlightColor : Theme.primaryColor
+ }
+ menu: ContextMenu {
+ MenuItem {
+ text: qsTr("Edit")
+ onClicked: {
+ var pg = pageStack.animatorPush(Qt.resolvedUrl("ProfileEditor.qml"),
+ {
+ profileName : name,
+ useProtocol : use_protocol,
+ useSubdomain : use_subdomains,
+ useDomain : use_domain,
+ usePortPath : use_port_path,
+ passwordLength : password_length,
+ hashAlgorithm : hash_algorithm,
+ useLeet : use_leet,
+ leetLevel : leet_level > 0 ? leet_level : 1,
+ characters : characters,
+ username : username,
+ modifier : modifier,
+ prefix : prefix,
+ suffix : suffix,
+ useUserInfo : use_user_info,
+ useDefaultFallbackForProtocol : use_undefined_as_protocol_fallback
+ });
+ pg.pageCompleted.connect(function(pg) {
+ pg.accepted.connect(function() {
+ name = pg.profileName;
+ use_protocol = pg.useProtocol;
+ use_subdomains = pg.useSubdomain;
+ use_domain = pg.useDomain;
+ use_port_path = pg.usePortPath;
+ password_length = pg.passwordLength;
+ hash_algorithm = pg.hashAlgorithm;
+ use_leet = pg.useLeet;
+ leet_level = pg.leetLevel;
+ characters = pg.characters;
+ username = pg.username;
+ modifier = pg.modifier;
+ prefix = pg.prefix;
+ suffix = pg.suffix;
+ use_user_info = pg.useUserInfo;
+ use_undefined_as_protocol_fallback = pg.useDefaultFallbackForProtocol;
+
+ if(index === profilesView.model.current_profile)
+ passwordmaker.profile_changed();
+
+ profilesView.store_profile_with_error_message();
+ })
+ })
+ }
+ }
+ MenuItem {
+ text: qsTr("Remove")
+ enabled: profilesView.count > 1
+ onClicked: {
+ delegate.remorseDelete(function() {
+ var bWasCurrentProfile = index === profilesView.model.current_profile;
+
+ profilesView.model.removeRows(index,1);
+ profilesView.store_profile_with_error_message();
+
+ if(bWasCurrentProfile)
+ passwordmaker.profile_changed();
+ })
+ }
+ }
+ }
+ onClicked: {
+ if(profilesView.model.current_profile !== index)
+ {
+ profilesView.model.current_profile = index;
+ passwordmaker.profile_changed();
+ }
+ pageContainer.navigateBack(PageStackAction.Animated);
+ }
+ }
+ NoticeOptional {
+ id: storeFailureNotice
+ text: qsTr("Saving profiles failed.")
+ useNotificationFallback: true
+ }
+
+ VerticalScrollDecorator {}
+ }
+}
diff --git a/qml/pages/SettingsEditor.qml b/qml/pages/SettingsEditor.qml
new file mode 100644
index 0000000..9baa51c
--- /dev/null
+++ b/qml/pages/SettingsEditor.qml
@@ -0,0 +1,90 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+
+Dialog {
+ id: settingsEditor
+
+ property var clear_generated_password;
+ property var clear_master_password;
+ property alias hide_generated_password: hide_generated_passwordBox.checked;
+
+ onAccepted: {
+ clear_generated_password =
+ clear_generated_passwordBox.checked
+ ? clear_generated_password_time.sliderValue * 60
+ : null;
+ clear_master_password =
+ clear_master_passwordBox.checked
+ ? clear_master_password_time.sliderValue * 60
+ : null;
+ }
+
+
+ SilicaFlickable {
+ anchors.fill: parent
+ contentHeight: column.height
+
+ VerticalScrollDecorator {}
+
+ Column {
+ id: column
+ width: parent.width
+ bottomPadding: Theme.paddingLarge
+ DialogHeader {
+ title: qsTr("Edit Settings")
+ }
+ TextSwitch {
+ id: hide_generated_passwordBox
+ text: qsTr("Hide Generated Password")
+ palette.highlightColor : Theme.highlightColor
+ highlighted: down
+ }
+ TextSwitch {
+ id: clear_generated_passwordBox
+ text: qsTr("Auto-clear generated password")
+ palette.highlightColor : Theme.highlightColor
+ highlighted: down
+ checked: typeof clear_generated_password !== 'undefined'
+ }
+ Slider {
+ id: clear_generated_password_time
+ minimumValue: 1
+ maximumValue: 15
+ stepSize: 1
+ width: parent.width
+ valueText: value + " min"
+ visible: clear_generated_passwordBox.checked
+ label: qsTr("Auto-clear generated pass timeout")
+ value: clear_generated_password/60 || 1
+ }
+ TextSwitch {
+ id: clear_master_passwordBox
+ text: qsTr("Auto-clear master password")
+ palette.highlightColor : Theme.highlightColor
+ highlighted: down
+ checked: typeof clear_master_password !== 'undefined'
+ }
+ Slider {
+ id: clear_master_password_time
+ minimumValue: clear_generated_passwordBox.checked
+ ? clear_generated_password_time.sliderValue
+ : 1
+ maximumValue: 30
+ stepSize: 1
+ width: parent.width
+ valueText: sliderValue + " min"
+ visible: clear_master_passwordBox.checked
+ label: qsTr("Auto-clear master pass timeout")
+ value: clear_master_password/60 || 5
+ }
+ Label {
+ topPadding: Theme.paddingLarge
+ width: parent.width - 2*Theme.horizontalPageMargin
+ x: Theme.horizontalPageMargin
+ wrapMode: Text.WordWrap
+ color: Theme.highlightColor
+ text: qsTr("Profiles can be edited directly in the profiles selector.")
+ }
+ }
+ }
+}
diff --git a/rpm/harbour-passfish.changes.in b/rpm/harbour-passfish.changes.in
new file mode 100644
index 0000000..db9a565
--- /dev/null
+++ b/rpm/harbour-passfish.changes.in
@@ -0,0 +1,18 @@
+# Rename this file as PassFish.changes to include changelog
+# entries in your RPM file.
+#
+# Add new changelog entries following the format below.
+# Add newest entries to the top of the list.
+# Separate entries from eachother with a blank line.
+#
+# Alternatively, if your changelog is automatically generated (e.g. with
+# the git-change-log command provided with Sailfish OS SDK), create a
+# PassFish.changes.run script to let mb2 run the required commands for you.
+
+# * date Author's Name version-release
+# - Summary of changes
+
+* Sun Apr 13 2014 Jack Tar 0.0.1-1
+- Scrubbed the deck
+- Hoisted the sails
+
diff --git a/rpm/harbour-passfish.changes.run.in b/rpm/harbour-passfish.changes.run.in
new file mode 100644
index 0000000..9a9cc5d
--- /dev/null
+++ b/rpm/harbour-passfish.changes.run.in
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Rename this file as PassFish.changes.run to let mb2 automatically
+# generate changelog from well formatted Git commit messages and tag
+# annotations.
+
+git-change-log
+
+# Here are some basic examples how to change from the default behavior. Run
+# git-change-log --help inside the Sailfish OS SDK chroot or build engine to
+# learn all the options git-change-log accepts.
+
+# Use a subset of tags
+#git-change-log --tags refs/tags/my-prefix/*
+
+# Group entries by minor revision, suppress headlines for patch-level revisions
+#git-change-log --dense '/[0-9]+.[0-9+$'
+
+# Trim very old changes
+#git-change-log --since 2014-04-01
+#echo '[ Some changelog entries trimmed for brevity ]'
+
+# Use the subjects (first lines) of tag annotations when no entry would be
+# included for a revision otherwise
+#git-change-log --auto-add-annotations
diff --git a/rpm/harbour-passfish.spec b/rpm/harbour-passfish.spec
new file mode 100644
index 0000000..d574bfa
--- /dev/null
+++ b/rpm/harbour-passfish.spec
@@ -0,0 +1,69 @@
+#
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.32
+#
+
+Name: harbour-passfish
+
+# >> macros
+%define __provides_exclude_from ^%{_datadir}/.*$
+%define __requires_exclude ^libpassfish.so$
+# << macros
+
+Summary: PassFish
+Version: 0.1
+Release: 1
+Group: Security/Accounts
+License: GPL
+URL: https://github.com/soulsource
+Source0: %{name}-%{version}.tar.bz2
+Source100: harbour-passfish.yaml
+Requires: sailfishsilica-qt5 >= 0.10.9
+BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
+BuildRequires: pkgconfig(Qt5Core)
+BuildRequires: pkgconfig(Qt5Qml)
+BuildRequires: pkgconfig(Qt5Quick)
+BuildRequires: desktop-file-utils
+BuildRequires: cmake
+
+%description
+A reimplementation of PasswordMaker Pro, aiming to be mostly compatible.
+
+
+%prep
+%setup -q -n %{name}-%{version}
+
+# >> setup
+# << setup
+
+%build
+# >> build pre
+# << build pre
+
+%cmake .
+make %{?_smp_mflags}
+
+# >> build post
+# << build post
+
+%install
+rm -rf %{buildroot}
+# >> install pre
+# << install pre
+%make_install
+
+# >> install post
+# << install post
+
+desktop-file-install --delete-original \
+ --dir %{buildroot}%{_datadir}/applications \
+ %{buildroot}%{_datadir}/applications/*.desktop
+
+%files
+%defattr(-,root,root,-)
+%{_bindir}
+%{_datadir}/%{name}
+%{_datadir}/applications/%{name}.desktop
+%{_datadir}/icons/hicolor/*/apps/%{name}.png
+# >> files
+# << files
diff --git a/rpm/harbour-passfish.yaml b/rpm/harbour-passfish.yaml
new file mode 100644
index 0000000..2c47d4c
--- /dev/null
+++ b/rpm/harbour-passfish.yaml
@@ -0,0 +1,42 @@
+Name: harbour-passfish
+Summary: PassFish
+Version: 0.1
+Release: 1
+# The contents of the Group field should be one of the groups listed here:
+# https://github.com/mer-tools/spectacle/blob/master/data/GROUPS
+Group: Security/Accounts
+URL: https://github.com/soulsource
+License: GPL
+# This must be generated before uploading a package to a remote build service.
+# Usually this line does not need to be modified.
+Sources:
+- '%{name}-%{version}.tar.bz2'
+Description: |
+ A reimplementation of PasswordMaker Pro, aiming to be mostly compatible.
+Builder: cmake
+
+# This section specifies build dependencies that are resolved using pkgconfig.
+# This is the preferred way of specifying build dependencies for your package.
+PkgConfigBR:
+ - sailfishapp >= 1.0.2
+ - Qt5Core
+ - Qt5Qml
+ - Qt5Quick
+
+# Build dependencies without a pkgconfig setup can be listed here
+# PkgBR:
+# - package-needed-to-build
+
+# Runtime dependencies which are not automatically detected
+Requires:
+ - sailfishsilica-qt5 >= 0.10.9
+
+# All installed files
+Files:
+ - '%{_bindir}'
+ - '%{_datadir}/%{name}'
+ - '%{_datadir}/applications/%{name}.desktop'
+ - '%{_datadir}/icons/hicolor/*/apps/%{name}.png'
+
+# For more information about yaml and what's supported in Sailfish OS
+# build system, please see https://wiki.merproject.org/wiki/Spectacle
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000..8d8ed52
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,452 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "downcast"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fragile"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2"
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mockall"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d4d70639a72f972725db16350db56da68266ca368b2a1fe26724a903ad3d6b8"
+dependencies = [
+ "cfg-if",
+ "downcast",
+ "fragile",
+ "lazy_static",
+ "mockall_derive",
+ "predicates",
+ "predicates-tree",
+]
+
+[[package]]
+name = "mockall_derive"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79ef208208a0dea3f72221e26e904cdc6db2e481d9ade89081ddd494f1dbaa6b"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "mockall_double"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dffc15b97456ecc84d2bde8c1df79145e154f45225828c4361f676e1b82acd6"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "passfish"
+version = "1.0.0"
+dependencies = [
+ "dirs",
+ "libc",
+ "mockall",
+ "mockall_double",
+ "passwordmaker-rs",
+ "passwordmaker_macros",
+ "ripemd",
+ "rust_testhelper",
+ "serde",
+ "toml",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "passwordmaker-rs"
+version = "0.1.0"
+source = "git+https://github.com/soulsource/passwordmaker-rs.git#fd68c7ad50b78f84443e826fbe29bce24c417dd7"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "passwordmaker_macros"
+version = "1.0.0"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "predicates"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
+dependencies = [
+ "difflib",
+ "float-cmp",
+ "itertools",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "ripemd"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "rust_testhelper"
+version = "0.1.0"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "termtree"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..85c38d7
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "passfish"
+version = "1.0.0"
+edition = "2018"
+
+[dependencies]
+libc = "0.2"
+serde = { version = "1.0", features = ["derive", "rc"] }
+toml = "0.5"
+dirs = "4.0"
+unicode-segmentation = "1.10.0"
+mockall_double = "0.2"
+ripemd = {version = "0.1.3", features = ["std"] }
+passwordmaker_macros = { path = "../rust_macro" }
+passwordmaker-rs = { git = "https://github.com/soulsource/passwordmaker-rs.git" }
+
+[dev-dependencies]
+mockall = "0.11"
+rust_testhelper = { path = "../rust_testhelper" }
+
+[lib]
+name = "passfish"
+crate-type = ["cdylib"]
diff --git a/rust/clippy.toml b/rust/clippy.toml
new file mode 100644
index 0000000..935336a
--- /dev/null
+++ b/rust/clippy.toml
@@ -0,0 +1 @@
+msrv = "1.52.0"
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 {
+ 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 for LoadError {
+ fn from(e : std::io::Error) -> Self {
+ LoadError::Loading(e)
+ }
+}
+impl From 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 for StoreError {
+ fn from(e: std::io::Error) -> Self {
+ StoreError::Writing(e)
+ }
+}
+impl From 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 {
+ sender : Sender,
+ emit : E,
+}
+
+impl EmittingSender{
+ pub(crate) fn emitting_channel(emit : E) -> (Self, Receiver) {
+ let (sender,r) = channel();
+ (EmittingSender { sender, emit }, r)
+ }
+ pub(crate) fn send(&self, t: MT) -> Result<(), SendError> {
+ //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);
+
+pub(crate) type QtMd4 = QHasher;
+pub(crate) type QtMd5 = QHasher;
+pub(crate) type QtSha1 = QHasher;
+pub(crate) type QtSha256 = QHasher;
+
+impl Hasher for QHasher 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) -> Result {
+ 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(first_message: I, other_messages: It) -> I::Output
+ where I: GetNewerOrMoreImportantCommand,
+ It: Iterator
+{
+ other_messages.fold(first_message.convert_first_command(), I::get_newer_or_more_important_command)
+}
+
+fn receive_and_log_error(receiver : &Receiver) -> Result {
+ 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(to_ui : &EmittingSender, from_ui : &Receiver) -> Result<(), SendError>{
+ 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,
+ used_text : RefCell,
+ master_password : RefCell,
+ generated_password : RefCell,
+ generator_state : RefCell,
+ from_helper : Receiver,
+ to_helper : Sender,
+ helper_thread : Option>
+}
+
+#[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 for GeneratorState {
+ type Error = GeneratorStateFromIntError;
+ fn try_from(value: u8) -> Result {
+ 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 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(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.generated_password.borrow());
+ }
+
+ fn master_password(&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(&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(&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 = 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,
+ clear_master_password_seconds : Option,
+ 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
+}
+
+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{
+ 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 {
+ self.data.borrow().clear_generated_password_seconds
+ }
+ fn set_clear_generated_password_seconds(&self, value: Option) {
+ 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 {
+ self.data.borrow().clear_master_password_seconds
+ }
+ fn set_clear_master_password_seconds(&self, value: Option) {
+ 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
+}
+
+pub(super) enum UiToHelper {
+ Shutdown,
+ GeneratePassword(GeneratePasswordTask),
+}
+
+pub(super) enum GenerationIssue {
+ Settings(SettingsError),
+ Input(GenerationError),
+}
+
+impl From for GenerationIssue {
+ fn from(s: SettingsError) -> Self {
+ GenerationIssue::Settings(s)
+ }
+}
+
+impl From 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 for u8 {
+ fn from(h : HashAlgorithm) -> u8 {
+ h as u8
+ }
+}
+impl TryFrom for HashAlgorithm {
+ type Error = ();
+ fn try_from(i : u8) -> Result {
+ 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 for u8 {
+ fn from(l : LeetLevel) -> u8 {
+ l as u8 + 1
+ }
+}
+impl TryFrom for LeetLevel {
+ type Error = ();
+ fn try_from(i : u8) -> Result {
+ 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 {
+ 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,
+ url_parsing_settings : UrlParsingSettings,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Profile {
+ name : String,
+ settings : ProfileSettings,
+}
+
+#[derive(Serialize, Deserialize)]
+struct StoredProfiles {
+ profiles : Vec,
+}
+
+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 {
+ 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,
+
+ current_profile_idx : RefCell,
+}
+
+#[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(&self, operation : F) -> Result
+ 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, 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(&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(&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(&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(&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(&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(&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(&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::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 = (1+LeetLevel::variant_count() as u8).try_into();
+ assert!(z.is_err());
+ let a : Result = 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
diff --git a/rust/src/interface.rs b/rust/src/interface.rs
new file mode 100644
index 0000000..e065f22
--- /dev/null
+++ b/rust/src/interface.rs
@@ -0,0 +1,1062 @@
+/* generated by rust_qt_binding_generator */
+use libc::{c_char, c_ushort, c_int};
+use std::slice;
+use std::char::decode_utf16;
+
+use std::sync::Arc;
+use std::sync::atomic::{AtomicPtr, Ordering};
+use std::ptr::null;
+
+#[cfg(test)]
+use mockall::automock;
+
+use crate::implementation::*;
+
+
+#[repr(C)]
+pub struct COption {
+ data: T,
+ some: bool,
+}
+
+impl COption {
+ #![allow(dead_code)]
+ fn into(self) -> Option {
+ if self.some {
+ Some(self.data)
+ } else {
+ None
+ }
+ }
+}
+
+impl From